fix: answer unreadable request formats with 415, and count content types (DW-985)
Merged
CR
15h ago+122+122+012345678901234567890123456789-18-18-01234567890123456789
Stacked on #2082. Completes DW-985.
Why
RFC 9110 §15.5.16 reserves 415 for a request whose content format or coding the server doesn't support. #2082 returns it for unsupported content codings. Three other refusals in the same class still came back as a 400, which tells the client its request was malformed rather than sent in the wrong format:
POST /ai/v1/fileswithoutmultipart/form-data, e.g. a raw JSONL body. This was a 400 "Invalid multipart request: invalid boundary", and it took an upload slot before being refused.- A batch file compressed with xz, bzip2, zip or lz4. #2082 names the format but returned a 400.
- Strict-mode
/responseswith a body not declared as JSON. This was a 400invalid_content_type.
All three already fail today, so changing the status breaks no working client.
What changes
- dwctl: a new
Error::UnsupportedMediaTypevariant, returned as a 415. - File uploads: a request without a
multipart/form-datacontent type gets a 415 before it takes an upload slot. A multipart request with a broken form, such as a bad boundary, is still a 400. Files compressed in a format we don't decode also get a 415. - onwards, strict
/responses: a missing JSON content type returns 415. The code staysinvalid_content_type, soonwards_rejections_totalseries keep their label. - New metric:
dwctl_inference_request_content_types_total{content_type}counts the content type declared on each POST to the inference endpoints, i.e. everything under/ai/v1/*except the file and batch routes, which already check theirs. Values are grouped into json, form, multipart, text, other and none./chat/completions,/completionsand/embeddingsparse the body whatever it declares, andcurl -dwithout-Hdeclares a form body. Rejecting on content type there could break working clients, so this measures how many such requests there are before anyone decides.
Not in this PR
- Rejecting non-JSON content types on the lenient endpoints: a follow-up, once the new metric shows how much traffic it would affect.
- Non-UTF-8
charsetparameters: deliberately left alone for the same reason.
Testing
test_upload_that_is_not_a_multipart_form_returns_415: a JSONL body, a form body and a missing content type each get a 415. A multipart body with a broken form still gets a 400.test_upload_broken_or_unsupported_compression_names_the_problem: xz gets a 415, and truncated gzip still gets a 400.responses_bodies_not_sent_as_json_are_counted_as_suchin onwards: now asserts a 415 and the unchanged rejection code.classifies_declared_content_types: covers the metric's labels.