Skip to content
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

Merged
merged 3 commits into from
Jul 18, 2024

Conversation

dimitribouniol
Copy link
Contributor

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.

Comment on lines 110 to 135
@_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.
Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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

@dimitribouniol
Copy link
Contributor Author

@swift-server-bot test this please

@dimitribouniol dimitribouniol force-pushed the dimitri/conditional-compression branch from 088a0e5 to f398c71 Compare July 9, 2024 11:02
@dimitribouniol dimitribouniol force-pushed the dimitri/conditional-compression branch from f398c71 to 1972e77 Compare July 12, 2024 08:11
@Lukasa Lukasa added the 🆕 semver/minor Adds new public API. label Jul 17, 2024
Copy link
Contributor

@Lukasa Lukasa left a 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.

Comment on lines 110 to 135
@_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.
Copy link
Contributor

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// - 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.

Copy link
Contributor Author

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()
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

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)

@dimitribouniol dimitribouniol force-pushed the dimitri/conditional-compression branch from 1972e77 to 006187b Compare July 17, 2024 20:31
/// 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)
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated!

@dimitribouniol dimitribouniol force-pushed the dimitri/conditional-compression branch from 006187b to 2f2678e Compare July 18, 2024 13:12
@Lukasa
Copy link
Contributor

Lukasa commented Jul 18, 2024

@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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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

@dimitribouniol dimitribouniol force-pushed the dimitri/conditional-compression branch from 2f2678e to eab1303 Compare July 18, 2024 13:33
Copy link
Contributor

@Lukasa Lukasa left a 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!

@Lukasa Lukasa merged commit d1ead62 into apple:main Jul 18, 2024
6 of 7 checks passed
@dimitribouniol dimitribouniol deleted the dimitri/conditional-compression branch July 18, 2024 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants