[swift-server-dev] HTTP API v0.1.0
johannesweiss at apple.com
Thu Oct 5 06:30:59 CDT 2017
> On 4 Oct 2017, at 11:33 pm, Jack Lawrence via swift-server-dev <swift-server-dev at swift.org> wrote:
> I disagree that we should hold public API to a lower standard when we expect the number of clients of the API to be small.
> In particular, this API is the foundation for server-side Swift. It is especially important for this API to be simple, clear, and unambiguous. Confusing API leads to bugs which leads to security vulnerabilities.
> API contract enforcement
> One major concern I have with the API as it stands is that it’s too easy to write incorrect code. Whenever possible, the API contract should be defined in the type system rather than implicitly (in documentation or through runtime errors). In this case, the write* functions must be called in a particular order. For example, if you write body data before header data, or write header data then body data then some more header data, the header data is silently dropped—the implementation of writeHeaders just early returns without any indication that you did something wrong. Surprisingly, the completion handler isn’t invoked either which could lead to memory that’s never cleaned up or file descriptors that are left open.
I agree with your points. I haven't checked the mailing list but didn't we anyway agree that we wanted to only have a 'writeResponseHead` which you pass a full `HTTPResponseHead` value which contains the whole head (status, headers, ...)? If you have a good suggestion how to improve this here without being less versatile of an API I'm sure people would listen.
For us the current API works quite well as it's only used as the lower level. We have nicer to use (albeit less versatile) high-level APIs for actual user code. In the OSS world, that'd be Vapor, Kitura etc.
> This is the equivalent of Objective-C’s runtime behavior when a method is called on nil. It leads to difficult to find bugs and vulnerabilities, and it’s why Swift defines away the problem using the type system and runtime assertions.
our implementation `preconditionFailure`s if
- you lose the last reference to the response writer and haven't call done
- you call the functions out of order (`writeHeaders` after `writeBody`, `writeBody` after `done`, ...)
Whilst that is not ideal, with the lack of a fancy type system (linear types or so) I failed to encode all that into the type system without sacrificing performance or versatility. I'm not questioning we could encode a little more in the type system though (and I think we should).
> When you encounter an API design issue like this, there are two general approaches:
> A) Runtime reporting of programmer error, via error handling or some other mechanism.
> B) API design that codifies the pattern.
> In this case, I would strongly advocate for an API design that codifies the pattern in the type system. I’m not sure exactly what the right answer is here because I’m not super familiar with server-side development, but fundamentally I think the framework should *ask* for data rather than being directed to write it.
That seems infeasible unfortunately for the sending side (the HTTP response). Imagine a long-poll server. The client makes a request and maybe an hour later the server responds with a state change. Say you have an API for football (⚽️) scores, nothing happens for a while and then someone scores. The server will then 'send' (as a chunk in Chunked-Encoding HTTP body) a bit of JSON to the client which then changes the UI. The request will stay alive and when the next goal happens, the server will send another bit of JSON.
So if the framework were to ask the user for data there's two options:
1) a call into user code that (synchronously) returns what we want to send
2) a call into the user code with a callback that the user is supposed to call asynchronously when there's data to be sent
(1) doesn't work as you'd block a kernel thread per request which is a non-starter
(2) suffers from the same problem as the current API: the user might just never call that callback :\
I'm all ears if you have ideas though.
> I could imagine a streaming API or a set of callbacks that are called in the correct order with a mechanism to e.g. tell the framework that you’re “done” providing header or body data. I disagree that this should be left up to a higher level API.
can you expand here? A general streaming API would be very nice indeed but there isn't one that we can/want to depend on. The networking/streams part of the Swift Server APIs workgroup is looking into this. Unfortunately, as it's less concrete than a low-level HTTP/1.1 server API, there's even more opinions and it's even harder to settle on one design :).
> The pattern used to write data leads to code that’s difficult to read. In particular, there seem to be two ways of writing data—using the writeBody function and/or returning a closure using HTTPBodyProcessing.
no, `writeBody` is the only way to write a HTTP response body. `HTTPBodyProcessing` is there to process the HTTP request body. It'll be called whenever there are bytes available from the client. Pleas note it might take any length of time for the client to finish sending its request body. And the server may start streaming the response body any amount of time before the client finishes sending its request body.
> Having two ways to do the same thing but one has extra information available to it leads to code that’s hard to read (data writing/execution is linear, and then suddenly there’s code that’s executed later, one or more times). It also makes the code harder to refactor. It’s also not clear what the difference is between finishedProcessing(), stop = true, and response.done(). Why do I need to call any of those?
`stop` was introduced because of discussions on this list. `stop` means "I'm no longer interested in any further request body chunks sent from the client". That allows the library to efficiently discard them and facilitates the development of server apps (less state management). The `response.done()` means I'm done writing the HTTP response body.
It's never mandatory to do anything with `stop` but it is mandatory to call `response.done()` when you're done. `done`'s callback will be called whenever everything has been sent (or failed to send) and you get an overall result if it was successful.
> Patterns that encode control flow in the API lead to hard to debug errors and complicated implicit control flow. For example, what happens if I call done() or abort()
That needs to be documented. `done()` should finish the HTTP response. `abort()` will finish it ASAP (without trying to send anything outstanding) and close the connection. `done()` will leave it open (for the next keep-alive request).
> , but then write some body data?
our implementation crashes if you violate the state machine.
> What happens if I forget to call finishProcessing?
well, you'll never get a new body chunk from the client. Our implementation crashes if you leak (or call more than once) the `finishProcessing` callback.
> What if I want to throw an error?
you can't as it's an asynchronous API and Swift doesn't have support for that yet (will probably come with async-await).
> Generators, iterators, and streams again provide good examples of using the language’s control flow rather than creating your own stateful system.
Swift doesn't have language support for that, as mentioned above we didn't want to design our own stream abstraction. That's also not trivial as you will need to establish back pressure from the client to the server. Ie. not buffer data if the client sends quicker than the server can consume.
> It’s not clear to me how one actually replaces one or more of the built-in types with their own that conform to the protocols. Can you provide an example or explain how you expect the protocols to be used? Why are all the protocols class-constrained?
IIRC only the `HTTPResponseWriter` is class-constrained that is because the `HTTPResponseWriter` has identity and therefore should be a `class` in Swift. It has reference semantics and not value semantics as it has state and (at least transitively) holds a file descriptor.
> Rather than take closures directly, could the API instead use protocols with requirements that match the closure signature? APIs that directly ask for closures tend to encourage large functions and large types, whereas protocol-based APIs encourage small, isolated types. For example, HTTPServer.start could take an object that defines a handler function, rather than taking the handler directly (this is especially important for `handler`, which could contain a lot of logic). Another example is HTTPBodyProcessing, which takes a processing function rather than a type that knows how to process.
the idea is that you usually wrap that API to make it more user-friendly. But please go ahead and propose a protocol based one.
I hope that makes sense. Please don't get me wrong, this is all not perfect and not set in stone.
>> On Oct 3, 2017, at 12:59 PM, Helge Heß via swift-server-dev <swift-server-dev at swift.org> wrote:
>> On 3. Oct 2017, at 19:55, Vladimir.S via swift-server-dev <swift-server-dev at swift.org> wrote:
>>> Thank you Chris and others for all of this job, I believe HTTP API is very important for Swift future.
>>> For example, the 'echo' server written in Node.js I googled fast:
>> Googling fast is not always Googling well ;-)
>> (Note that this is not the suggested Node.js style anymore either. To support back pressure, you are supposed to use readable events. This is a good intro on the relevant stuff: https://github.com/substack/stream-handbook)
>> Anyways, I think this effort is not about streams at all, it is about providing a basic HTTP server library, not a complete framework like Node.js.
>> The intention is that you would build an actual developer-facing framework on top of that. And while I somewhat dislike the proposed API, I think it is good enough as a basis to layer another API on top of it ;-)
>> On 3. Oct 2017, at 21:20, Georgios Moschovitis via swift-server-dev <swift-server-dev at swift.org> wrote:
>>> Indeed, I also find the HTMLBodyProcessing interface peculiar.
>>> A ‘stream’ based interface like NodeJS looks more familiar to me.
>>> Maybe we should revisit the API after the outcome of the concurrency/async-await/reactive-streams
>>> design process?
>> I think that the interface is weird is not necessarily a blocker, as long as you can build an async stream based interface on top of it, and that seems to be a given.
>> The concurrency/async-await may make stuff nicer, but it wouldn’t change the actual implementation?
>> What I read about `reactive-streams` may not be (very well) suitable for I/O. To me it sounds a little like Node streams v1 vs v3. Do you have a link explaining this stuff? I’m interested in learning more about this (is this a Swift 5 proposal or something?)
>> P.S.: If you want Node.js like (v3) streams, you may consider having a look at my Noze.io project (http://noze.io). It is an attempt to reimplement the Node streaming features in Swift, while preserving type safety. Well, and enhance it to streams of arbitrary objects (Node also supports that, kinda, but you loose all the batch processing/buffering features, making it kinda non-sensical)
>> P.S.2.: I didn’t look through it, but the demo implementation looks very weird to me. It seems to use DispatchQueues as if they are threads, and in combination w/ that, locks?! I’m probably unfair, I just had a cursory look :->
>> swift-server-dev mailing list
>> swift-server-dev at swift.org
> swift-server-dev mailing list
> swift-server-dev at swift.org
More information about the swift-server-dev