A request works while you send JSON. You change the client to FormData, send the same logical fields, and the API starts returning 415 Unsupported Media Type.
The important change is the body format. The endpoint may accept JSON but not multipart form data. A 415 usually means the server does not accept the format you sent—not necessarily that the values or field names are wrong.
If the endpoint accepts application/json and you send multipart/form-data instead, you are asking it to read a different kind of request. The fields may look identical in your code, but the bytes sent over HTTP are not. The server is allowed to reject that representation with 415.
Check the media type accepted by the endpoint against the request body and Content-Type header your client actually sends.
Check which request format the endpoint accepts
HTTP 415 means the origin server is refusing the request because its content is in a format that the target resource does not support. The mismatch can involve the request Content-Type, Content-Encoding, or the body format itself.
Suppose this request works:
await fetch("/api/profile", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Sneha",
role: "developer",
}),
});
The endpoint receives a JSON representation:
Content-Type: application/json
{"name":"Sneha","role":"developer"}
Now the client changes to FormData:
const form = new FormData();
form.append("name", "Sneha");
form.append("role", "developer");
await fetch("/api/profile", {
method: "POST",
body: form,
});
The browser no longer sends JSON. Fetch serializes the body as multipart form data and supplies a generated boundary in the Content-Type header.
Conceptually, the request now looks like this:
Content-Type: multipart/form-data; boundary=----generated-boundary
------generated-boundary
Content-Disposition: form-data; name="name"
Sneha
------generated-boundary
Content-Disposition: form-data; name="role"
developer
------generated-boundary--
If /api/profile accepts only application/json, returning 415 is reasonable. The fields may be logically equivalent, but the representation is not.
Compare the working JSON request with the failing FormData request
I reproduced the request-shape change against a small local HTTP endpoint using Node 22.16.0 with its built-in Fetch and FormData implementation.
The endpoint was deliberately strict: one route accepted only application/json; another accepted only multipart/form-data with a boundary.
| Client request | Endpoint expectation | Observed result |
|---|---|---|
| JSON body + application/json | JSON | 200 |
| FormData body | JSON | 415 |
| FormData body | multipart | 200 |
| FormData body + manually forced Content-Type: multipart/form-data | multipart with boundary | 415 in this strict test |
| JSON body + application/x-www-form-urlencoded header | JSON | 415 |
The important part is not the exact status every framework will choose. Different servers and middleware can return 400, 415, or an application-specific error for a malformed or unexpected body.
The useful observation is that body type, declared media type, and server expectation have to agree.
Diagnose a 415 in this order
1. Find out what the endpoint accepts
Check the API documentation, OpenAPI definition, server route, controller annotation, middleware configuration, or a previously working request.
You are looking for something concrete such as:
-
application/json
-
multipart/form-data
-
application/x-www-form-urlencoded
-
more than one accepted media type
Do not infer this from the data shape alone. An object with name and role can be represented as JSON, multipart fields, or URL-encoded fields. The endpoint decides which representations it understands.
A 415 response may also include a header that hints at accepted formats, although you should not depend on every API returning one.
2. Inspect the request that left the client
Look at the actual request in browser DevTools or your HTTP client, not only the source code that built it.
Check:
-
Request Content-Type
-
Whether a multipart boundary is present
-
The request payload/body
-
Whether your client library transformed the body
-
Whether a wrapper or interceptor changed headers
This catches a common debugging mistake: the code says “FormData”, but a shared HTTP client still injects Content-Type: application/json from a default configuration.
3. Compare the body type with the media type
These are three different request representations:
JSON
await fetch("/api/profile", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Sneha", role: "developer" }),
});
URL-encoded form data
const body = new URLSearchParams({
name: "Sneha",
role: "developer",
});
await fetch("/api/profile", {
method: "POST",
body,
});
The Fetch Standard assigns a URLSearchParams body the media type application/x-www-form-urlencoded;charset=UTF-8 when the request does not already contain a Content-Type header.
Multipart FormData
const form = new FormData();
form.append("name", "Sneha");
form.append("role", "developer");
await fetch("/api/profile", {
method: "POST",
body: form,
});
For FormData, Fetch serializes the entries as multipart/form-data and generates the boundary used to separate the parts.
These formats are not interchangeable merely because they carry the same field names.
Do not “fix” FormData by forcing the multipart header
A common reaction to a 415 is:
const form = new FormData();
form.append("name", "Sneha");
await fetch("/api/profile", {
method: "POST",
headers: {
"Content-Type": "multipart/form-data",
},
body: form,
});
That can create a second problem.
When Fetch receives a FormData body, it generates the multipart boundary and normally adds a Content-Type such as:
Content-Type: multipart/form-data; boundary=----some-generated-value
If you manually set only multipart/form-data, the body still contains boundary markers but the header may not tell the server what boundary to parse. MDN specifically warns against explicitly setting the Content-Type header when sending FormData with Fetch or XMLHttpRequest for this reason.
So there are two separate questions:
-
Does the endpoint accept multipart at all?
-
If it does, is the multipart request correctly framed with its boundary?
Do not use a header tweak to hide the first problem.
If the endpoint accepts JSON, keep JSON
If the API contract says the route consumes JSON and you are not uploading files or using a form-specific contract, the correct fix may simply be to keep the request as JSON.
await fetch("/api/profile", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Sneha",
role: "developer",
}),
});
Converting an object to FormData is not an upgrade by itself. It is useful only when it matches what the receiving endpoint expects.
This is especially important with nested objects and arrays. JSON has explicit object and array types. FormData has an ordered list of field names whose values are strings or File/Blob objects, so nesting requires a naming or serialization convention that the server must understand.
If you need to inspect those field mappings before sending anything, the JSON to Form Data Converter can show the flattened fields and form encodings without making a network request.
If the endpoint accepts multipart, let FormData be multipart
For an endpoint that actually accepts multipart/form-data, keep the Fetch request simple:
const form = new FormData();
form.append("name", "Sneha");
form.append("role", "developer");
await fetch("/api/profile", {
method: "POST",
body: form,
});
Do not add Content-Type manually unless you are deliberately constructing the multipart body yourself and understand the framing requirements.
If files are involved, append a real File or Blob. A path-looking string is still only a string.
form.append("avatar", fileInput.files[0]);
The endpoint still has to be configured to consume multipart data and to use field names that match the server’s parser or binding rules.
If the endpoint expects URL-encoded fields, FormData is still the wrong body
Some form-style endpoints accept application/x-www-form-urlencoded, not multipart.
For simple text fields, URLSearchParams is often a better match:
const body = new URLSearchParams({
grant_type: "client_credentials",
scope: "read",
});
await fetch("/oauth/token", {
method: "POST",
body,
});
Again, the deciding factor is the endpoint contract. “Form-like data” does not imply one universal HTTP representation.
415 is not the same problem as a bad multipart boundary
These two failures are easy to mix together:
Unsupported representation
The endpoint expects JSON, but the client sends multipart. A 415 is a natural response.
Malformed multipart representation
The endpoint accepts multipart, but the request is missing or has an unusable boundary. A server might return 400, 415, or another parser error depending on its implementation.
That distinction matters because the fixes are different.
If the endpoint does not accept multipart, fixing the boundary will not make multipart valid for that route.
If the endpoint does accept multipart but the boundary is broken, changing the endpoint contract is unnecessary; fix how the multipart request is generated.
What to check when a 415 appears
When a previously working JSON request starts returning 415 after moving to FormData:
-
Confirm the media types the endpoint actually accepts.
-
Inspect the outgoing Content-Type in DevTools.
-
Inspect the outgoing body, not just the JavaScript object that created it.
-
If the body is FormData, check that Content-Type includes a generated boundary.
-
Remove a manually forced multipart header when the browser should generate it.
-
If the route accepts only JSON, send JSON instead of trying to force multipart through it.
-
If it expects URL-encoded form fields, use the representation the endpoint requires.
-
Only after the media type is correct, debug field names, nesting, arrays, null handling, and file parts.
That order prevents a lot of wasted debugging. There is little value in fixing nested field notation while the server is rejecting the entire representation before it reaches field parsing.
Where the converter helps — and where it does not
Yoryantra’s JSON to Form Data Converter is useful when the endpoint already expects form-style input and you need to reason about field names, arrays, nulls, multipart framing, or URL-encoded output.
It does not tell a server to accept FormData, and it does not change an API endpoint’s media-type contract.
If the response is 415, solve the representation mismatch first. Then use the converter to make the fields match the server’s form-data contract.
For inspecting a raw HTTP request after that, the HTTP Request Formatter can help separate the request line, headers, and body without sending the request.
Sources and test basis
-
RFC 9110, section 15.5.16 — definition and semantics of 415 Unsupported Media Type.
-
MDN — 415 Unsupported Media Type and
Content-Typerequest behavior. -
MDN — Using the Fetch API and Using FormData Objects.
-
WHATWG Fetch Standard — body extraction rules for FormData and URLSearchParams, including generated media types and multipart boundaries.
-
Local reproduction run on 7 October 2026 with Node 22.16.0 built-in Fetch/FormData against a strict local HTTP endpoint. The reproduction was used to verify request media types and boundary behavior; real server status codes can vary by framework and parser configuration.
