-
Notifications
You must be signed in to change notification settings - Fork 125
Always overwrite Transport-Encoding
and Content-Length
#479
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Always overwrite Transport-Encoding
and Content-Length
#479
Conversation
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just out of curiosity, why is this generic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be private and all tests should go directly through validateAndFixTransportFraming
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to @fabianfett's note.
} | ||
|
||
private func validateFieldNames() throws { | ||
func validateFieldNames() throws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this internal now?
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
func testContentLengthHeaderIsChangedIfBodyHasDifferentLength() { | |
var headers = HTTPHeaders([("Content-Length", "0")]) | |
var metadata: RequestFramingMetadata? | |
XCTAssertNoThrow(metadata = try headers.validateAndFixTransportFraming(method: .PUT, bodyLength: .fixed(length: 200))) | |
XCTAssertEqual(headers.first(name: "Content-Length"), "200") | |
XCTAssertEqual(metadata?.body, .fixedSize(200)) | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need @Lukasa's wisdom here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
/// Create and stream body using `StreamWriter`. | |
/// | |
/// - parameters: | |
/// - length: Body size. Request validation will be failed with `HTTPClientErrors.contentLengthMissing` if nil, | |
/// unless `Transfer-Encoding: chunked` header is set. | |
/// - stream: Body chunk provider. | |
public static func stream(length: Int? = nil, _ stream: @escaping (StreamWriter) -> EventLoopFuture<Void>) -> Body { | |
return Body(length: length, stream: stream) | |
} |
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
/// Create and stream body using `StreamWriter`. | |
/// | |
/// - parameters: | |
/// - length: Body size. Request validation will be failed with `HTTPClientErrors.contentLengthMissing` if nil, | |
/// unless `Transfer-Encoding: chunked` header is set. | |
/// - stream: Body chunk provider. | |
public static func stream(length: Int? = nil, _ stream: @escaping (StreamWriter) -> EventLoopFuture<Void>) -> Body { | |
return Body(length: length, stream: stream) | |
} |
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to @fabianfett's note.
// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, this looks so much better already. Tiny nits.
@@ -937,6 +939,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible { | |||
/// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add a message here like: "AsyncHTTPClient now silently corrects invalid headers."
@@ -959,6 +962,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible { | |||
/// 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above
|
||
func testTraceMethodIsNotAllowedToHaveAFixedLengthBody() { | ||
var headers = HTTPHeaders() | ||
XCTAssertThrowsError(try headers.validateAndSetTransportFraming(method: .TRACE, bodyLength: .fixed(length: 10))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the errors are equatable would you mind checking, we get what we expect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huge improvement!
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-Encoding
andContent-Length
headers are processed. We concluded that a user should not be allowed to setTransport-Encoding
orContent-Length
headers themselves. A user can only communicate its intend through thebody
property on a request. We infer the correct and most optimalTransport-Encoding
orContent-Length
header for them.Changes
Transport-Encoding
andContent-Length
or no longer validated and always removed or replacedvalidate
tovalidateAndSetTransportFraming
because it does not only validate but also mutate theHTTPHeaders
to include the correct headers for the given request body.RequestBodyLength
which can be either.dynamic
if 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.Body
but rather a new typeHTTPRequest.Body
.HTTPClient.Request.body
is 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
RequestBodyLength
and just use and optionalInt
.