-
Notifications
You must be signed in to change notification settings - Fork 82
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
Conditional Response Compression #225
Conditional Response Compression #225
Conversation
@_disfavoredOverload | ||
@available(*, deprecated, message: "Deprecated in favor of HTTPResponseCompressor(initialByteBufferCapacity:, responseCompressionPredicate:)") | ||
public convenience init(initialByteBufferCapacity: Int = 1024) { | ||
// TODO: This version is kept around for backwards compatibility and should be merged with the signature below in the next major version. |
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.
Not sure what the best practice is here for deprecating overloads that can break backwards compatibility. I remember this was a thing early on in Swift 5, but not sure if its still relevant?
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.
Let's not deprecate this: we can just have two methods and remove the default argument for the new 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.
Should I keep the todo? Is there another mechanism to not forget these on the next major version bump?
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.
Please go ahead and file an issue and we can mark it for the next major version, but I'm happy to keep the TODO as well.
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.
Linked in the code as well: #226
@swift-server-bot test this please |
088a0e5
to
f398c71
Compare
f398c71
to
1972e77
Compare
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.
Generally looks really nice. Just a few nits here.
@_disfavoredOverload | ||
@available(*, deprecated, message: "Deprecated in favor of HTTPResponseCompressor(initialByteBufferCapacity:, responseCompressionPredicate:)") | ||
public convenience init(initialByteBufferCapacity: Int = 1024) { | ||
// TODO: This version is kept around for backwards compatibility and should be merged with the signature below in the next major version. |
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.
Let's not deprecate this: we can just have two methods and remove the default argument for the new one.
/// - Parameter initialByteBufferCapacity: Initial size of buffer to allocate when hander is first added. | ||
public init(initialByteBufferCapacity: Int = 1024) { | ||
/// - Parameter responseCompressionPredicate: The predicate used to determine if the response should be compressed or not based on its headers. Defaults to `nil`, which will compress every response this handler sees. This predicate is always called wether the client supports compression for this response or not, so it can be used to clean up any marker headers you may use to determine if compression should be performed or not. Please see ``ResponseCompressionPredicate`` for more details. |
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.
/// - Parameter responseCompressionPredicate: The predicate used to determine if the response should be compressed or not based on its headers. Defaults to `nil`, which will compress every response this handler sees. This predicate is always called wether the client supports compression for this response or not, so it can be used to clean up any marker headers you may use to determine if compression should be performed or not. Please see ``ResponseCompressionPredicate`` for more details. | |
/// - Parameter responseCompressionPredicate: The predicate used to determine if the response should be compressed or not based on its headers. Defaults to `nil`, which will compress every response this handler sees. This predicate is always called whether the client supports compression for this response or not, so it can be used to clean up any marker headers you may use to determine if compression should be performed or not. Please see ``ResponseCompressionPredicate`` for more details. |
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 testConditionalCompressionEnabled() throws { | ||
let predicateWasCalled = expectation(description: "Predicate was called") | ||
let compressor = HTTPResponseCompressor { responseHeaders, isCompressionSupported in | ||
predicateWasCalled.fulfill() |
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 probably fulfil the predicate last.
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.
Put them inside a defer instead so this is guaranteed, unless that would lead to a bad outcome? Its only really trying to make sure the predicate isn't silently skipped.
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.
Defer is just fine.
/// - Parameter isCompressionSupported: Set to `true` if the client requested compatible compression, and if the HTTP response supports it, otherwise `false`. | ||
/// - Returns: Return `true` if the compressor should proceed to compress the response, or `false` if the response should not be compressed. | ||
/// | ||
/// - Note: Returning `true` when compression is not supported will not enable compression, and the modified headers will always be used. |
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.
This behaviour is surprising to me. I think we should introduce a new enum for the return value, something like compressIfPossible
and doNotCompress
as the cases. Just to clarify what will happen.
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 was thinking about this too, glad we are on the same page. Are we ok with enum
enums, or should I make a struct with private enums for future compatibility?
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.
Went with a struct for futureproofness after all, but let me know if the cases are clearly named (went with an "intent"-like terminology, and clarified the wording in the doc comment)
1972e77
to
006187b
Compare
/// The response should be compressed if supported by the HTTP protocol. | ||
public static let compressibleResponse = CompressionIntent(.compressibleResponse) | ||
/// The response should not be compressed even if supported by the HTTP protocol. | ||
public static let uncompressedResponse = CompressionIntent(.uncompressedResponse) |
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 let's make it clear these are action requests:
.compressIfPossible
.doNotCompress
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.
Updated!
006187b
to
2f2678e
Compare
@swift-server-bot add to allowlist |
/// Initialise a ``HTTPResponseCompressor`` | ||
/// Initialize a ``HTTPResponseCompressor``. | ||
/// - Parameter initialByteBufferCapacity: Initial size of buffer to allocate when hander is first added. | ||
public convenience init(initialByteBufferCapacity: Int) { |
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.
Hmm, when I suggested taking off the default argument value I actually meant to take it from the other initializer's responseCompressionPredicate
. But I now wonder: why can't we have default values here? There should be no ambiguity, the compiler should be fine.
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.
Initially I ran into trouble, but I forgot what the issue was — let me give it another try
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.
You're right, we can have default arguments for both of them after all, let me add it back
2f2678e
to
eab1303
Compare
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 love this, this is fantastic work. Thanks @dimitribouniol!
Added support for conditional response compression in
HTTPResponseCompression
.Motivation:
This change allows libraries like vapor or direct users of the handler to add additional functionality around when a response should be compressed, rather than always or never for the channel's lifetime. This is particularly important for pipelined requests, as some responses (ie. dynamic data) should be dynamically compressed, while others (ie. images) should not.
Two specific use cases immediately came to mind, particularly checking the content type of the response and using some heuristic, or using marker headers (ie.
X-Vapor-Compression
) to statelessly mark entire routes as compressible or incompressible by default.Modifications:
Rather than hardcode specific use cases, I went with an approach other handlers have used which is to provide a predicate to
HTTPResponseCompression
that allows the caller to weigh in on if compression should be performed. This allows a library like vapor to always install the handler, but then conditionally check the headers of the response about to go out to determine if it should be compressed or not based on heuristics and knobs it can choose to expose. Note that this predicate is always called even in cases where the request does not support compression, so the user of the handler does not need to concern themselves ahead of time whether or not to write marker headers based on the requests, and can keep the logic and cleanup contained in one place.Result:
HTTPResponseCompression
now supports conditional response compression allowing for greater flexibility in how the handler is used, which can hopefully increase its general usefulness.