Always overwrite Transport-Encoding and Content-Length#479
Always overwrite Transport-Encoding and Content-Length#479dnadoba merged 6 commits intoswift-server:mainfrom
Transport-Encoding and Content-Length#479Conversation
| let encodings = self[canonicalForm: "Transfer-Encoding"] | ||
|
|
||
| // "Transfer-Encoding" and "Content-Length" are not allowed to present at the same time (https://tools.ietf.org/html/rfc7230#section-3.3.1) | ||
| guard encodings.isEmpty || contentLength == nil else { |
There was a problem hiding this comment.
Super NIT: I think an if is easier to read here.
| static func validateTransferEncoding<Encodings>( | ||
| _ encodings: Encodings | ||
| ) throws where Encodings: Sequence, Encodings.Element: StringProtocol { |
There was a problem hiding this comment.
Just out of curiosity, why is this generic?
There was a problem hiding this comment.
We call this function during validateAndFixTransportFraming with [Substring] but it is more convenience to use [String] in tests.
There was a problem hiding this comment.
I think this should be private and all tests should go directly through validateAndFixTransportFraming.
| } | ||
|
|
||
| private func validateFieldNames() throws { | ||
| func validateFieldNames() throws { |
| self.add(name: "Content-Length", value: String(length)) | ||
| return .init(connectionClose: connectionClose, body: .fixedSize(length)) | ||
| } else { | ||
| self.add(name: "Transfer-Encoding", value: encodings.joined(separator: ", ")) |
There was a problem hiding this comment.
I think we should make this explicit with a comment.
Even though a fixed length body is going to be sent, the user explicitly set an encoding. We should respect the users wish and use the specified headers.
| throw HTTPClientError.traceRequestWithBody | ||
| } | ||
| if encodings.isEmpty { | ||
| self.add(name: "Content-Length", value: String(length)) |
There was a problem hiding this comment.
There might be an interesting case here: What if the user handed us a fixed size body payload, but also set the Content-Length header to something different?
There was a problem hiding this comment.
I once had validation in there but noticed that we actually have tests that check that we always overwrite the Content-Length header. I would also prefer to validate it but this might break user code. WDYT?
There was a problem hiding this comment.
There was a problem hiding this comment.
We should absolutely always override it. The user is not entitled to set that header: it doesn't belong to them.
| // if a user forgot to specify a Content-Length and Transfer-Encoding, we will set it for them | ||
| self.add(name: "Transfer-Encoding", value: "chunked") | ||
| } else { | ||
| self.add(name: "Transfer-Encoding", value: encodings.joined(separator: ", ")) |
There was a problem hiding this comment.
We also run in this case if the user has set a Content-Length header explicitly. I think in the dynamic case we should trust a user set content length header (though we should validate it is a positive int).
A case might be streaming a file somewhere.
There was a problem hiding this comment.
I'm not really sure about this. A user can always specify the length of a stream upfront through our API and doesn't need to set the Content-Length header manually through the length parameter.
async-http-client/Sources/AsyncHTTPClient/HTTPHandler.swift
Lines 61 to 69 in 62ad818
This would give them a way to do the same thing in to different ways and they may conflict.
I would almost say that we should always ignore/remove "Content-Length" header.
The same is almost true for
Transport-Encoding but I'm not fully understanding all use cases for this one.
There was a problem hiding this comment.
If I read this code snipped correctly, Transport-Encoding can only be chunked because SwiftNIO always replace the Transport-Encoding with chunked or doesn't do chunked encoding if it is not the only encoding.
https://github.com/apple/swift-nio/blob/addf69cfe60376c325397c8926589415576b1dd1/Sources/NIOHTTP1/HTTPEncoder.swift#L104-L129
There was a problem hiding this comment.
we should always ignore/remove "Content-Length" header
Nope, definitely not. I know at least the S3 apis, that require a Content-Length even for large uploads, if you deal with pre-signed links and friends.
There was a problem hiding this comment.
Yeah, we don't support other transfer encodings in NIO and so we should probably stop doing so here. However, I don't know if making that change in this PR is worthwhile.
As to supporting Content-Length: yes, we should emit it, but we should do so when users tell us the size of their stream. We shouldn't tolerate them setting that header.
There was a problem hiding this comment.
As to supporting Content-Length: yes, we should emit it, but we should do so when users tell us the size of their stream. We shouldn't tolerate them setting that header.
Okay, I'm happy with that!
In that case we need to fix the code comment though here:
async-http-client/Sources/AsyncHTTPClient/HTTPHandler.swift
Lines 61 to 69 in 62ad818
If no Content-Length header is set, we will just reach for chunked.
62ad818 to
539e4fd
Compare
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| enum RequestBodyLength: Equatable { |
There was a problem hiding this comment.
Nit: As a matter of style I think it's rarely useful to have : Equatable. Whenever you feel like writing that, you may as well also write : Hashable.
There was a problem hiding this comment.
Is the binary size increase negligible for the additional synthesised conformance? AFAIK Swift doesn't remove unused synthesised conformances, even for internal types.
There was a problem hiding this comment.
Yeah, this is not the most fruitful way of shrinking binary sizes.
| method: HTTPMethod, | ||
| bodyLength: RequestBodyLength | ||
| ) throws -> RequestFramingMetadata { | ||
| let contentLength = self.first(name: "Content-Length") |
There was a problem hiding this comment.
Why are we getting the value if we don't need it?
| throw HTTPClientError.traceRequestWithBody | ||
| } | ||
| if encodings.isEmpty { | ||
| self.add(name: "Content-Length", value: String(length)) |
There was a problem hiding this comment.
We should absolutely always override it. The user is not entitled to set that header: it doesn't belong to them.
| let contentLength = self.first(name: "Content-Length") | ||
| let encodings = self[canonicalForm: "Transfer-Encoding"] | ||
|
|
||
| // "Transfer-Encoding" and "Content-Length" are not allowed to present at the same time (https://tools.ietf.org/html/rfc7230#section-3.3.1) |
There was a problem hiding this comment.
Nit:
| // "Transfer-Encoding" and "Content-Length" are not allowed to present at the same time (https://tools.ietf.org/html/rfc7230#section-3.3.1) | |
| // "Transfer-Encoding" and "Content-Length" are not allowed to be present at the same time (https://tools.ietf.org/html/rfc7230#section-3.3.1) |
| static func validateTransferEncoding<Encodings>( | ||
| _ encodings: Encodings | ||
| ) throws where Encodings: Sequence, Encodings.Element: StringProtocol { |
| // if a user forgot to specify a Content-Length and Transfer-Encoding, we will set it for them | ||
| self.add(name: "Transfer-Encoding", value: "chunked") | ||
| } else { | ||
| self.add(name: "Transfer-Encoding", value: encodings.joined(separator: ", ")) |
There was a problem hiding this comment.
Yeah, we don't support other transfer encodings in NIO and so we should probably stop doing so here. However, I don't know if making that change in this PR is worthwhile.
As to supporting Content-Length: yes, we should emit it, but we should do so when users tell us the size of their stream. We shouldn't tolerate them setting that header.
539e4fd to
afad35f
Compare
afad35f to
311d927
Compare
Transport-Encoding and Content-Length
fabianfett
left a comment
There was a problem hiding this comment.
Wow, this looks so much better already. Tiny nits.
| /// Request contains invalid identity encoding. | ||
| public static let identityCodingIncorrectlyPresent = HTTPClientError(code: .identityCodingIncorrectlyPresent) | ||
| /// Request contains multiple chunks definitions. | ||
| @available(*, deprecated) |
There was a problem hiding this comment.
We should add a message here like: "AsyncHTTPClient now silently corrects invalid headers."
| /// Body part was written after request was fully sent. | ||
| public static let writeAfterRequestSent = HTTPClientError(code: .writeAfterRequestSent) | ||
| /// Incompatible headers specified, for example `Transfer-Encoding` and `Content-Length`. | ||
| @available(*, deprecated) |
|
|
||
| func testTraceMethodIsNotAllowedToHaveAFixedLengthBody() { | ||
| var headers = HTTPHeaders() | ||
| XCTAssertThrowsError(try headers.validateAndSetTransportFraming(method: .TRACE, bodyLength: .fixed(length: 10))) |
There was a problem hiding this comment.
Since the errors are equatable would you mind checking, we get what we expect?
Motivation
We want to reuse request validation in the upcoming async/await implementation. During refactoring we noticed some inconsistency in the current implementation on how user defined
Transport-EncodingandContent-Lengthheaders are processed. We concluded that a user should not be allowed to setTransport-EncodingorContent-Lengthheaders themselves. A user can only communicate its intend through thebodyproperty on a request. We infer the correct and most optimalTransport-EncodingorContent-Lengthheader for them.Changes
Transport-EncodingandContent-Lengthor no longer validated and always removed or replacedvalidatetovalidateAndSetTransportFramingbecause it does not only validate but also mutate theHTTPHeadersto include the correct headers for the given request body.RequestBodyLengthwhich can be either.dynamicif we do not now the size of a request in advance or.fixed(length: Int)if the user has specified the length of the body.HTTPClient.Request.Body?but rather a new typeRequestBodyLength. This allows use to reuse the function for async/await because we will not useHTTPClient.Request.Bodybut rather a new typeHTTPRequest.Body.HTTPClient.Request.bodyis set to nil) and a request body with a length of0(e.g where body is.byteBuffer(ByteBuffer())are now processed the same way.Alternatives
We could also remove
RequestBodyLengthand just use and optionalInt.