trpc/trpc · error · TRPCError

UNSUPPORTED_MEDIA_TYPE

UNSUPPORTED_MEDIA_TYPE

Error message

Unsupported content-type "${req.headers.get('content-type')}

What it means

`getContentTypeHandler` matches the request against registered handlers (JSON, formData, octet-stream). If none match and the method is not GET (GET falls back to JSON so endpoints open in a browser), the request is rejected as UNSUPPORTED_MEDIA_TYPE, echoing the offending content-type.

Source

Thrown at packages/server/src/unstable-core-do-not-import/http/contentType.ts:307

const handlers = [
  jsonContentTypeHandler,
  formDataContentTypeHandler,
  octetStreamContentTypeHandler,
];

function getContentTypeHandler(req: Request): ContentTypeHandler {
  const handler = handlers.find((handler) => handler.isMatch(req));
  if (handler) {
    return handler;
  }

  if (!handler && req.method === 'GET') {
    // fallback to JSON for get requests so GET-requests can be opened in browser easily
    return jsonContentTypeHandler;
  }

  throw new TRPCError({
    code: 'UNSUPPORTED_MEDIA_TYPE',
    message: req.headers.has('content-type')
      ? `Unsupported content-type "${req.headers.get('content-type')}`
      : 'Missing content-type header',
  });
}

export async function getRequestInfo(
  opts: GetRequestInfoOptions,
): Promise<TRPCRequestInfo> {
  const handler = getContentTypeHandler(opts.req);
  return await handler.parse(opts);
}

View on GitHub (pinned to acff82332d)

Solutions

  1. Send `application/json` (or one of the supported content-types) from the client.
  2. Ensure client and server versions agree on the supported content-type set.
  3. For raw uploads use `application/octet-stream` with POST; for form uploads use `multipart/form-data` with POST.

Example fix

// before
fetch('/api/trpc/user.get', {
  method: 'POST',
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
  body: 'id=1',
})

// after
fetch('/api/trpc/user.get', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ id: 1 }),
})
Defensive patterns

Strategy: validation

Validate before calling

// Validate content-type against the supported set
const SUPPORTED = ['application/json', 'multipart/form-data', 'application/octet-stream'];
function assertSupportedContentType(ct: string | null): void {
  if (!ct || !SUPPORTED.some((s) => ct.startsWith(s))) {
    throw new Error(`Unsupported content-type: ${ct}`);
  }
}

Type guard

function isSupportedContentType(ct: string | null): boolean {
  return !!ct && ['application/json', 'multipart/form-data', 'application/octet-stream']
    .some((s) => ct.startsWith(s));
}

Try / catch

try {
  await trpc.proc.mutate(body);
} catch (e) {
  if (e instanceof TRPCError && e.code === 'UNSUPPORTED_MEDIA_TYPE') {
    // Switch the request to application/json (or another supported type)
  }
  throw e;
}

Prevention

When it happens

Trigger: Sending an unsupported type such as `text/xml` or `application/x-www-form-urlencoded` with a non-GET method; a content-type with parameters that defeat the `startsWith` matcher.

Common situations: Wrong serializer on the client; a client defaulting to form-urlencoded; a version mismatch where a handler was removed or renamed.

Related errors


AI-assisted analysis of trpc/trpc@acff82332d (2026-08-12). Data as JSON: /api/errors/4ce953eb3a137286. Report an issue: GitHub.