Git Product home page Git Product logo

Comments (12)

Lukasa avatar Lukasa commented on September 24, 2024 1

Yeah the current API is definitely limited: unfortunately, that limitation basically comes from the fact that there doesn't appear to be a really wonderful set of API choices that work nicely.

The most obvious alternative API design is to follow the pattern established in the protocol upgrader, which is that you'd have a constructor like this:

public init(_ shouldCompress: (HTTPRequestHead, HTTPResponseHead) -> Bool)

This would, for each response head, ask whether the response itself should be compressed. This is a strict improvement over the current model, but has the disadvantage of requiring the user do some awkward state coordination if they can't determine whether they should compress based purely on the header block. It's unquestionably an improvement over the current API, but it's not necessarily a great fit.

More generally this handler is not a good fit for many projects. It has a bunch of other limitations:

  • no support for offline compression
  • no support for lazy offline compression of resources
  • blocks event loop to run compression, causing performance hits
  • frequently changes content-length-delimited responses to chunked responses

While we can design a better API for this handler, it's not clear to me that we should. In general for web frameworks I'd say that compression of resources is something the framework itself should be handling, and that it should not be part of the channel pipeline. Frameworks should support offline compression of static resources into multiple compressed formats such that those compressed responses can be served from disk, and should allow compression of dynamic resources based on meta-knowledge of the resource itself. This handler is not so much a general solution to that problem as it is an example, and something that can be used in situations where you are confident that all resources can reasonably be compressed (e.g. you only ship JSON responses).

from swift-nio-extras.

weissi avatar weissi commented on September 24, 2024

CC @kjessup

from swift-nio-extras.

weissi avatar weissi commented on September 24, 2024

I don't have a great idea either for an API either. Instead of a ChannelHandler maybe we want to provide convenience functions of the form (HTTPResponsePart) -> Future<HTTPResponsePart> that do the compression. These could come with options for example whether to compress on or off the event loop etc. That way we're moving it into the web framework itself but it's super low overhead for them to support it. To mimic the current behaviour the web framework could just replace

ctx.write(...(responsePart))

with

compressBody(responsePart).flatMap { ctx.write(...($0) }

and if they want to only selectively compress

(shouldCompress(reqHead) ? compressBody : identity)(responsePart).flatMap { ctx.write(...($0)) }

or something along those lines. So I agree we probably want to take this out of the pipeline.

from swift-nio-extras.

Lukasa avatar Lukasa commented on September 24, 2024

I think that raises a follow-on question for me: should we just be providing an encapsulation for compression in general? At this stage we're basically just wrapping zlib, so should we lean into that and just provide a useful API to wrapping zlib?

from swift-nio-extras.

helje5 avatar helje5 commented on September 24, 2024

I haven't been following this closely, but IMO compressing and decompressing channels are useful for many verbose HTTP APIs, e.g. WebDAV or anything JSON. This is separate to compressing (and storing/caching) large resources like say PDFs or images.
I'm not sure (and didn't do tests on that in the last 15 years or so), but I assume the compression overhead for API requests should be negligible and you wouldn't want to dispatch the compression outside the event loop. If anything, it should be faster than not having compression.

from swift-nio-extras.

Lukasa avatar Lukasa commented on September 24, 2024

@helje5 Definitely agreed, we're mostly just trying to work out what the set of primitives NIO provides should be, vs what frameworks should be handling themselves.

I see the questions as being as follows:

  1. Is the HTTPResponseCompressor with the behaviour as written today worth preserving?
  2. Should it be replaced by something smarter built in to NIO? If so, what should that look like?
  3. Should we provide better APIs to compression libraries to allow users to build this thing?

My current set of answers is:

  1. Probably yes, IMO it works nicely when you know you only have dynamic textual responses being passed around.
  2. Probably not: I struggle to conceive of an API that will be nice enough to drive from a web framework, which is the entity that ultimately needs to decide whether to apply compression or not.
  3. Probably no: we aren't in the business of shipping compression libraries.

But I don't hold any of these positions strongly, which is why I'm interested in kicking the idea around here and seeing what others think. I'm happy to be swayed on any of those answers.

from swift-nio-extras.

helje5 avatar helje5 commented on September 24, 2024

The more you think about it, the harder it gets. There are also the API issues around TE vs Content-Encoding (and the effects of the latter, e.g. potentially different ETags, ranges etc).
A channel which always compresses the whole host/port really just doesn't feel right and might encourage doing the wrong thing (much like the "back pressure" handler, sorry 😬).

My feeling is that you would definitely want compression handlers in NIO. But to make it work, I think you would need to be able to create stacked pipelines? So that you can create a new one which is just active for the current request and add the compression handler if necessary. So I wonder whether the API discussion is more an issue with the current "pipeline API" (e.g. in Node you can just freely nest streams).
(don't you have to do some form of nested pipeline for HTTP/2 anyways?)

from swift-nio-extras.

Lukasa avatar Lukasa commented on September 24, 2024

There are also the API issues around TE vs Content-Encoding (and the effects of the latter, e.g. potentially different ETags, ranges etc).
A channel which always compresses the whole host/port really just doesn't feel right and might encourage doing the wrong thing (much like the "back pressure" handler, sorry 😬).

All of this is why I don't think any complex implementation of this belongs in a channel handler. It just requires too much knowledge that is present at the framework level and that would have to be communicated to NIO. All of this just so that NIO can call deflate for you. It simply doesn't seem worth it to me.

It seems more generally useful to have it be easy for frameworks to call deflate in circumstances where they want to, and then to have NIO simply assume that bytes are bytes. All of the decision making here is just above NIO's pay grade.

But to make it work, I think you would need to be able to create stacked pipelines? So that you can create a new one which is just active for the current request and add the compression handler if necessary.

This is definitely an approach that can be taken. It wouldn't be hard to write a channel handler that creates a small sub-channel for each request/response. This is different to what we do in HTTP/2, because HTTP/2 fans-out one channel to many sub-channels, rather than one to one, but it's not hard to do.

But it seems to be that what we're doing here is middlewares. Almost all web frameworks today are shipping some form of middleware API, and the vast majority of them do not put their middleware onto the event loop. One certainly could, but I don't know if NIO should be pushing that as the expected design pattern. A web framework that wants to build things this way could certainly be built (/me looks meaningfully at µExpress), but I don't know if NIO should be shipping that.

from swift-nio-extras.

Lukasa avatar Lukasa commented on September 24, 2024

After further discussion with @helje5, I think a more useful construction may be this:

  1. Deprecate HTTPResponseCompressor.

    I don't think anyone is happy with how this works today, so we may as well shoot it in the head now.

  2. Write a new HTTPResponseCompressor that is "one-shot".

    Specifically, this will change the HTTPResponseCompressor to only ever be able to compress one response. It will also be explicitly told which compression algorithm to use to compress the response, rather than deploying its current compression detection logic.

  3. Write a new HTTP channel handler that creates a sub-channel per request.

    This would make it much easier to write things that look roughly like "middlewares", but that operate in the context of the channel pipeline. Rather than put them into the main channel pipeline and worry about how to handle that, they could be placed into the sub-channel pipeline, knowing full-well that the sub-channel will live for one and only one HTTP request.

    This handler would probably require the use of the HTTPServerPipelineHandler to keep code complexity down.

How do people feel about this approach?

from swift-nio-extras.

weissi avatar weissi commented on September 24, 2024

Sounds quite good but we need to assess how many allocations etc that'll cause per request.

from swift-nio-extras.

helje5 avatar helje5 commented on September 24, 2024

Thats is actually a fair point. So maybe some "should-I-do-my-job-callback" API is the better way to go.

from swift-nio-extras.

weissi avatar weissi commented on September 24, 2024

CC @normanmaurer

from swift-nio-extras.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.