Relic keeps Content-Type on Body (Body.bodyType), not in Headers. The direction is right: the type describes the bytes, a response cannot send a type that disagrees with its body, and copyWith(body: ...) keeps the two together. Working on form and multipart parsing (#380) turned up gaps that make the model leak.
Gaps
- Parameters were missing.
MimeType and BodyType had no media type parameters, so there was nowhere to put a boundary. Forms added ContentTypeHeader to work around it, and multipart/byteranges responses from StaticHandler went out over dart:io without a boundary, so clients could not split them. The forms PR adds BodyType.parameters and fixes the byteranges case.
encoding does not say what was sent. Body.fromDataStream and Body.fromData default text types to UTF-8. On a request, req.encoding is utf8 both when the client sent charset=utf-8 and when it sent no charset, so code cannot apply its own default.
- Too many types for one concept.
MimeType, BodyType and ContentTypeHeader, plus io.ContentType in the dart:io adapter. Each has its own parse and encode path.
- Two sources of truth on requests. The adapter copies the raw
content-type header into Headers and also into Body.bodyType. After copyWith(body: ...), headers.contentType and body.bodyType can disagree.
- Silent override on responses. Setting
headers['content-type'] on a response is dropped by the adapter with no error. HttpResponseExtension.applyHeaders always writes Content-Type from the body.
Proposal
- Make
BodyType the single media type: add BodyType.parse, keep the raw charset name next to encoding, and keep the parameters. ContentTypeHeader can then go.
- Give the request Content-Type one owner. Either the adapter leaves
content-type out of Headers, or headers.contentType reads from the body.
- Fail loudly when a response carries a raw
content-type header, for example an assert in debug mode.
Some of this is breaking for 2.0 users (req.encoding semantics, a raw header that used to be ignored now asserting).
Relic keeps Content-Type on
Body(Body.bodyType), not inHeaders. The direction is right: the type describes the bytes, a response cannot send a type that disagrees with its body, andcopyWith(body: ...)keeps the two together. Working on form and multipart parsing (#380) turned up gaps that make the model leak.Gaps
MimeTypeandBodyTypehad no media type parameters, so there was nowhere to put aboundary. Forms addedContentTypeHeaderto work around it, and multipart/byteranges responses fromStaticHandlerwent out over dart:io without a boundary, so clients could not split them. The forms PR addsBodyType.parametersand fixes the byteranges case.encodingdoes not say what was sent.Body.fromDataStreamandBody.fromDatadefault text types to UTF-8. On a request,req.encodingisutf8both when the client sentcharset=utf-8and when it sent no charset, so code cannot apply its own default.MimeType,BodyTypeandContentTypeHeader, plusio.ContentTypein the dart:io adapter. Each has its own parse and encode path.content-typeheader intoHeadersand also intoBody.bodyType. AftercopyWith(body: ...),headers.contentTypeandbody.bodyTypecan disagree.headers['content-type']on a response is dropped by the adapter with no error.HttpResponseExtension.applyHeadersalways writes Content-Type from the body.Proposal
BodyTypethe single media type: addBodyType.parse, keep the raw charset name next toencoding, and keep the parameters.ContentTypeHeadercan then go.content-typeout ofHeaders, orheaders.contentTypereads from the body.content-typeheader, for example an assert in debug mode.Some of this is breaking for 2.0 users (
req.encodingsemantics, a raw header that used to be ignored now asserting).