mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 04:42:10 +03:00
fix(api): return 415 on /v1/messages for non-JSON Content-Type via requireJsonContentType middleware (#6414) Integrated into release/v3.8.47. (thanks @chirag127)
90 lines
3.4 KiB
TypeScript
90 lines
3.4 KiB
TypeScript
// #6414: POST /v1/chat/completions (and /v1/messages) must return HTTP 415
|
|
// `unsupported_media_type` when the Content-Type header is not application/json,
|
|
// matching OpenAI's reference API and RFC 7231 §6.5.13. Previously OmniRoute
|
|
// silently parsed such requests as JSON via `.clone().json().catch(() => null)`
|
|
// and let them reach the provider-lookup layer, where they surfaced as misleading
|
|
// `model_not_found` / generic errors instead of a boundary 415.
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { requireJsonContentType } = await import(
|
|
"../../src/shared/middleware/requireJsonContentType.ts"
|
|
);
|
|
|
|
function makeRequest(method: string, contentType: string | null): Request {
|
|
const headers = new Headers();
|
|
if (contentType !== null) headers.set("content-type", contentType);
|
|
return new Request("http://localhost/v1/chat/completions", {
|
|
method,
|
|
headers,
|
|
body: method === "GET" || method === "HEAD" ? undefined : "{}",
|
|
});
|
|
}
|
|
|
|
test("returns 415 for text/plain on POST", async () => {
|
|
const rejection = requireJsonContentType(makeRequest("POST", "text/plain"));
|
|
assert.ok(rejection, "expected a rejection Response");
|
|
assert.equal(rejection.status, 415);
|
|
const body = await rejection.json();
|
|
assert.equal(body.error.code, "unsupported_media_type");
|
|
assert.equal(body.error.type, "invalid_request_error");
|
|
});
|
|
|
|
test("returns 415 when Content-Type header is missing on POST", async () => {
|
|
const rejection = requireJsonContentType(makeRequest("POST", null));
|
|
assert.ok(rejection, "missing content-type must be rejected");
|
|
assert.equal(rejection.status, 415);
|
|
});
|
|
|
|
test("returns 415 for application/x-www-form-urlencoded on POST", async () => {
|
|
const rejection = requireJsonContentType(
|
|
makeRequest("POST", "application/x-www-form-urlencoded")
|
|
);
|
|
assert.ok(rejection);
|
|
assert.equal(rejection.status, 415);
|
|
});
|
|
|
|
test("admits application/json", () => {
|
|
assert.equal(requireJsonContentType(makeRequest("POST", "application/json")), null);
|
|
});
|
|
|
|
test("admits application/json with charset suffix", () => {
|
|
assert.equal(
|
|
requireJsonContentType(makeRequest("POST", "application/json; charset=utf-8")),
|
|
null
|
|
);
|
|
});
|
|
|
|
test("admits Application/JSON (case-insensitive)", () => {
|
|
assert.equal(requireJsonContentType(makeRequest("POST", "Application/JSON")), null);
|
|
});
|
|
|
|
test("admits application/json with leading whitespace", () => {
|
|
assert.equal(requireJsonContentType(makeRequest("POST", " application/json")), null);
|
|
});
|
|
|
|
test("does not touch GET requests (no body → nothing to reject)", () => {
|
|
assert.equal(requireJsonContentType(makeRequest("GET", "text/plain")), null);
|
|
});
|
|
|
|
test("does not touch OPTIONS preflight", () => {
|
|
assert.equal(requireJsonContentType(makeRequest("OPTIONS", null)), null);
|
|
});
|
|
|
|
test("guards PUT and PATCH the same as POST", () => {
|
|
const putRejection = requireJsonContentType(makeRequest("PUT", "text/plain"));
|
|
assert.ok(putRejection);
|
|
assert.equal(putRejection.status, 415);
|
|
|
|
const patchRejection = requireJsonContentType(makeRequest("PATCH", "text/plain"));
|
|
assert.ok(patchRejection);
|
|
assert.equal(patchRejection.status, 415);
|
|
});
|
|
|
|
test("rejection carries CORS + JSON headers", () => {
|
|
const rejection = requireJsonContentType(makeRequest("POST", "text/plain"));
|
|
assert.ok(rejection);
|
|
assert.equal(rejection.headers.get("Content-Type"), "application/json");
|
|
assert.ok(rejection.headers.get("Access-Control-Allow-Methods"));
|
|
});
|