Proposal: Header fields as a type conforming to Encodable / Decodable


(George) #1

I know this has been discussed a few times on this list, but I’d like to throw a different model for decoding headers into the ring. My approach revolves around using the Codable protocols introduced in Swift 4. An example can be found here: https://github.com/GeorgeLyon/Server / https://github.com/GeorgeLyon/Server/blob/master/Sources/Server/main.swift

This approach is a happy medium between the swift-server/http specifying the headers and allowing the frameworks built on top of this core to be flexible in how they handle header fields. By providing a type, the framework can choose to use a [String:String] representation, or even a [(String, String)]. On the other hand, they could choose to use a Codable type with String values or even specialized types like [HTTPMediaRange] for Accept. Indeed, it may be beneficial for swift-server/http to provide default implementations of various header types like HTTPMediaRange without forcing a downstream framework into using them. These specialized types are however outside the scope of this proposal.

Also, decoding HTTP header fields and decoding JSON are VERY similar tasks, and it would be nice if they used the same underlying Swift mechanism so optimizations can be built once for both.

I can put together a PR if this is something the community thinks is interesting.
- George

P.S. Sorry for the double-mail but I thought this was different enough from my handlers-as-structs proposal that I wanted to separate the responses into two threads.


(Cory Benfield) #2

Sadly, they aren’t really.

JSON and XML are extremely well-specified formats that, amongst other things, are pretty well defined in the form of key-value data structures. HTTP headers are not like that, though they may appear that way superficially.

Let’s get one thing out of the way quickly: any HTTP implementation that uses [String: String] as the format of HTTP headers at the lowest level is spec-nonconformant, and will fail in a number of situations where it should succeed. The most notable case is cookies: the Set-Cookie header field can appear multiple times in a response but is not a header list and cannot be joined by any character as both comma (used elsewhere in HTTP) and semicolon (used by Cookie) are meaningful reserved characters in the syntax for Set-Cookie. Any attempt to represent Set-Cookie in [String: String] is doomed to failure.

As a more general note, attempts to think about HTTP header fields as a mapping tend to fall short. You *can* construct them this way, but they have a number of extensions that make them ill-suited to this simplified representation. The Python community has landed on the CaseInsensitiveOrderedMultiDict as the most informational representation for HTTP header fields: that is, a mapping that has case-insensitive keys, retains order, and has multiple values for each field.

All of this gets even trickier as you consider the sub-problems involved here. Consider what should happen for header fields that do not define a list format, but are nonetheless sent multiple times. Consider what should happen for header field values that include binary junk. Consider what should happen for header field values that use different text encodings than other header values in the same header block. Consider what should happen for header fields that are malformed but parseable (such as my favourite ever header line that I encountered in the wild, the four-octet sequence 3A 20 0D 0A “: \r\n”). Consider what should happen in cases where header field values that should conform to a specific format fail to do so.

All of these are complex application-specific error handling concerns, and all should be tolerated by the lowest-level implementations. This would not be an issue if Codable provided meaningful benefit that was not easily provided in another form, but it doesn’t really. All high-level implementations will likely accept the lowest-common-denominator implementation of headers (something like [(String, String)] or even [(String, Data)]) and provide a number of helper methods on top of it to transform these formats from their underlying representation to the higher-level one. This grants applications that want to be resilient to bad header fields the opportunity to do so, while allowing those that can reasonably assume they’ll handle well-formed data to provide the appropriate extensions that do the transformation.

This wouldn’t matter by itself either, but then we have to add that Codable is a whole lot of overhead that doesn’t provide us much advantage over the “base representation with extensions” model. Implementing the encoder/decoder for HTTP headers is a big chunk of work that needs to be handled by every single entity that attempts to provide the low-level API, and it won’t be faster than the base version, and it won’t provide much utility.

All-in-all I think I’m -1 on this: it’s a lot of complexity for relatively minimal gain.

Cory

···

On 11 Dec 2017, at 01:40, George Leontiev via swift-server-dev <swift-server-dev@swift.org> wrote:

I know this has been discussed a few times on this list, but I’d like to throw a different model for decoding headers into the ring. My approach revolves around using the Codable protocols introduced in Swift 4. An example can be found here: https://github.com/GeorgeLyon/Server / https://github.com/GeorgeLyon/Server/blob/master/Sources/Server/main.swift

This approach is a happy medium between the swift-server/http specifying the headers and allowing the frameworks built on top of this core to be flexible in how they handle header fields. By providing a type, the framework can choose to use a [String:String] representation, or even a [(String, String)]. On the other hand, they could choose to use a Codable type with String values or even specialized types like [HTTPMediaRange] for Accept. Indeed, it may be beneficial for swift-server/http to provide default implementations of various header types like HTTPMediaRange without forcing a downstream framework into using them. These specialized types are however outside the scope of this proposal.

Also, decoding HTTP header fields and decoding JSON are VERY similar tasks, and it would be nice if they used the same underlying Swift mechanism so optimizations can be built once for both.


(Helge Heß) #3

Hi George,

I know this has been discussed a few times on this list, but I’d like to throw a different model for decoding headers into the ring. My approach revolves around using the Codable protocols introduced in Swift 4. An example can be found here: https://github.com/GeorgeLyon/Server / https://github.com/GeorgeLyon/Server/blob/master/Sources/Server/main.swift

That is an interesting idea for sure!

Also, decoding HTTP header fields and decoding JSON are VERY similar tasks, and it would be nice if they used the same underlying Swift mechanism so optimizations can be built once for both.

I don’t they are very similar tasks at all, but I have no specific issues w/ abusing them for this :->
One is about decoding arbitrary nested structures of objects w/ rather fixed base types (and potentially back-references, hahaha, no ;->),
while the other one is about decoding a predefined, fixed structure of potentially arbitrary base values.

Also Decodable doesn’t really help us much w/ the part which actually requires decoding, i.e. properly decoding Cookie or If headers.

I think your suggestion is actually two kinda separate things:
1) latching on the framework level request representation to the
   HTTP framework layer and have the framework map them
2) use Decodable for mapping.

But lets take a step back and see what we are doing in the parsing process.

Right now:

1. grab a base-buffer, we receive data into it
2. trigger the parser, passes back pointers into base-buffer,
   we copy the data into a decoding-buffer until a token
   is complete
3. we convert the Data into a String and add it to the
   HTTPRequestHead object
4. if the head is complete, we trigger processing
5. potentially convert `HTTPRequestHead` into `FrameworkRequest`
   (but maybe just wrap or use as-is)

To use Decodable we would need to do this:

1. grab a base-buffer, we receive data into it
2. trigger the parser, passes back pointers into base-buffer,
   we copy the data into a decoding-buffer until a token
   is complete
3. keep the data, add it to a structure of essentially the
   form [ ( DataSlice, DataSlice ) ] - lets call it ‘HTTPRawRequestHead'
4. if the head is complete, call the decoder, convert the
   raw-request into the `FrameworkRequest`
5. trigger processing

What I like about your approach is, that we don’t need to create rather expensive String objects in step 3. E.g. decodeInt would just decode the int from a byte buffer - waaayy better :wink: This part is nice :slight_smile:
What I dislike about your approach is, that we need that transient structure we decode into. It could be more efficient than [(String,String)], e.g. [(Data,Data)], or maybe even something slice based on the base-buffer, but it still is a structure which captures the raw setup of the full request head.

I also have the feeling that Decodable is too heavyweight for the task (based on Strings etc), but that may not be justified and be demonstrated.

As mentioned before I’m actually a little against using Strings here, and my personal preference would actually be something like this:

1. grab a base-buffer, we receive data into it
2. trigger the parser, passes back pointers into base-buffer,
   we copy the data into a decoding-buffer until a token
   is complete
3. keep the data, add it to a structure of essentially the
   form [ ( DataSlice, DataSlice ) ] - lets call it ‘HTTPRawRequestHead’
4. if the head is complete, we trigger processing
5. potentially convert `HTTPRawRequestHead ` into `FrameworkRequest`
   (but maybe just wrap or use as-is!) - you could use Decoding here
   if you want, or not.

Or in other words: Even w/ Decodable we first need to decode into some complete header structure. What I would prefer over the other approaches is that we directly vend this lower level structure to the framework (it could still have stuff like `subscript(String)->String`!). Which can then decide what to do with it.
(I kinda envision that HTTPRawRequestHead as a single malloc buffer, and the header keys/values as just pointers into that)

Well, I think to go forward, I would suggest that you provide a test setup to measure the performance implications.
One should produce the current API's `HTTPRequestHead` using the current means (directly hooked up to the parser, incremental buildup of Strings), and one should produce the same structure using a Decoder based approach.
We could then compare the performance implications, and if they turn out to be negligible, I think it may be worthwhile to consider your approach for the added extra flexibility.

But this is just my opinion of course as an interested by-stander :wink:
  hh

P.S. Sorry for the double-mail but I thought this was different enough from my handlers-as-structs proposal that I wanted to separate the responses into two threads.

Absolutely fine IMO, those are distinct things.

···

On 11. Dec 2017, at 02:40, George Leontiev via swift-server-dev <swift-server-dev@swift.org> wrote:


(George) #4

@Cory
Using `Decodable` does not force the representation of header fields to be a dictionary. [HeaderField] can also be Decodable and the client can choose to handle the cases you bring up in the way they see fit and avoid many of the pitfalls you bring up.
`Decodable` provides a Swift-standard mechanism for the client to choose what representation of headers works for them. Want to use a basic [HeaderField] list? Great! Implementing a simple API and don’t really care about Set-Cookie or other nuances of the HTTP spec? This makes it easy! We can even provide “strategies” for repeated header field names similar to how JSONDecoder provides `dateDecodingStrategy`.
This approach also allows the framework to provide standard implementations of things like HTTPMediaType that web frameworks CAN use without preventing them from doing something else if it is necessary for their use case.

I do agree that the implementation of something like Codable has some overhead, is a nontrivial amount of work, and is currently missing some nice features like streaming-decoding. These are things that the Swift language WILL have to address at some point but, from my perspective, coding JSON over my own RPC mechanism or coding the (admittedly nuanced) HTTP header format are different flavors of the same problem and it would be great if they were handled in a similar way.

I could also see a world where both APIs are provided. An HTTPParser which can be used to parse token-by-token or an HTTPRequestParser which supports Codable headers.

- George

···

On Dec 11, 2017, at 2:43 AM, Cory Benfield <cbenfield@apple.com> wrote:

On 11 Dec 2017, at 01:40, George Leontiev via swift-server-dev <swift-server-dev@swift.org <mailto:swift-server-dev@swift.org>> wrote:

I know this has been discussed a few times on this list, but I’d like to throw a different model for decoding headers into the ring. My approach revolves around using the Codable protocols introduced in Swift 4. An example can be found here: https://github.com/GeorgeLyon/Server / https://github.com/GeorgeLyon/Server/blob/master/Sources/Server/main.swift

This approach is a happy medium between the swift-server/http specifying the headers and allowing the frameworks built on top of this core to be flexible in how they handle header fields. By providing a type, the framework can choose to use a [String:String] representation, or even a [(String, String)]. On the other hand, they could choose to use a Codable type with String values or even specialized types like [HTTPMediaRange] for Accept. Indeed, it may be beneficial for swift-server/http to provide default implementations of various header types like HTTPMediaRange without forcing a downstream framework into using them. These specialized types are however outside the scope of this proposal.

Also, decoding HTTP header fields and decoding JSON are VERY similar tasks, and it would be nice if they used the same underlying Swift mechanism so optimizations can be built once for both.

Sadly, they aren’t really.

JSON and XML are extremely well-specified formats that, amongst other things, are pretty well defined in the form of key-value data structures. HTTP headers are not like that, though they may appear that way superficially.

Let’s get one thing out of the way quickly: any HTTP implementation that uses [String: String] as the format of HTTP headers at the lowest level is spec-nonconformant, and will fail in a number of situations where it should succeed. The most notable case is cookies: the Set-Cookie header field can appear multiple times in a response but is not a header list and cannot be joined by any character as both comma (used elsewhere in HTTP) and semicolon (used by Cookie) are meaningful reserved characters in the syntax for Set-Cookie. Any attempt to represent Set-Cookie in [String: String] is doomed to failure.

As a more general note, attempts to think about HTTP header fields as a mapping tend to fall short. You *can* construct them this way, but they have a number of extensions that make them ill-suited to this simplified representation. The Python community has landed on the CaseInsensitiveOrderedMultiDict as the most informational representation for HTTP header fields: that is, a mapping that has case-insensitive keys, retains order, and has multiple values for each field.

All of this gets even trickier as you consider the sub-problems involved here. Consider what should happen for header fields that do not define a list format, but are nonetheless sent multiple times. Consider what should happen for header field values that include binary junk. Consider what should happen for header field values that use different text encodings than other header values in the same header block. Consider what should happen for header fields that are malformed but parseable (such as my favourite ever header line that I encountered in the wild, the four-octet sequence 3A 20 0D 0A “: \r\n”). Consider what should happen in cases where header field values that should conform to a specific format fail to do so.

All of these are complex application-specific error handling concerns, and all should be tolerated by the lowest-level implementations. This would not be an issue if Codable provided meaningful benefit that was not easily provided in another form, but it doesn’t really. All high-level implementations will likely accept the lowest-common-denominator implementation of headers (something like [(String, String)] or even [(String, Data)]) and provide a number of helper methods on top of it to transform these formats from their underlying representation to the higher-level one. This grants applications that want to be resilient to bad header fields the opportunity to do so, while allowing those that can reasonably assume they’ll handle well-formed data to provide the appropriate extensions that do the transformation.

This wouldn’t matter by itself either, but then we have to add that Codable is a whole lot of overhead that doesn’t provide us much advantage over the “base representation with extensions” model. Implementing the encoder/decoder for HTTP headers is a big chunk of work that needs to be handled by every single entity that attempts to provide the low-level API, and it won’t be faster than the base version, and it won’t provide much utility.

All-in-all I think I’m -1 on this: it’s a lot of complexity for relatively minimal gain.

Cory


(George) #5

Just found this while reading the HTTP spec (last paragraph of section 4.2): https://tools.ietf.org/html/rfc2616#section-4.2

It seems to insinuate that any repeated header which cannot be expressed as a list is invalid. I could not find documentation that Set-Cookie is special as @lukasa suggested. Barring a citation, I think this confirms that header fields CAN, in fact, be expressed as a [HeaderFieldName: HeaderFieldValue] dictionary and be spec-compliant.


(Cory Benfield) #6

RFC 2616 is not the HTTP specification any more. As noted in your hyperlink, that RFC was obsoleted by RFCs 7230, 7231, 7232, 7233, 7234, & 7235.

Turning to those, RFC 7230 § 3.2.2 is your friend here. Quoting the relevant passages in their entirety:

A sender MUST NOT generate multiple header fields with the same field name in a message unless either the entire field value for that header field is defined as a comma-separated list [i.e., #(values)] or the header field is a well-known exception (as noted below).

A recipient MAY combine multiple header fields with the same field name into one “field-name: field-value” pair, without changing the semantics of the message, by appending each subsequent field value to the combined field value in order, separated by a comma. The order in which header fields with the same field name are received is therefore significant to the interpretation of the combined field value; a proxy MUST NOT change the order of these field values when forwarding a message.

Note: In practice, the “Set-Cookie” header field ([RFC6265]) often
appears multiple times in a response message and does not use the
list syntax, violating the above requirements on multiple header
fields with the same name. Since it cannot be combined into a
single field-value, recipients ought to handle “Set-Cookie” as a
special case while processing header fields. (See Appendix A.2.3
of [Kri2001] for details.)

That’s the citation for Set-Cookie being a special case.


(George) #7

Ah, sorry… swimming through RFCs is hard. However, that language seems explicit that it is only “Set-Cookie” (a response-only header field) is special (indeed, the “in practice” makes it seem like this is an unfortunate consequence of existing implementations) So it would seem that a [HeaderFieldName: HeaderFieldValue] representation with special SetCookie affordance for responses would be valid.


(Cory Benfield) #8

What’s the special affordance for Set-Cookie look like?