[swift-server-dev] HTTP API Sketch v2

Helge Heß me at helgehess.eu
Fri Apr 7 06:18:15 CDT 2017


On 07 Apr 2017, at 12:24, Johannes Weiß <johannesweiss at apple.com> wrote:
>> - concrete types instead of protocols for HTTPRequest
>> - bad for layering this on top of alternative
>>   implementations
>> - it can still come with a default imp
> 
> ok, maybe we should offer both types of API?

That is my thinking, yes. A protocol and then a concrete default implementation to get people going.

>> - also: you could make NSURLRequest/Response support it
> I don't think they're very useful in their current state. For example:
> 
> - headers is a NSDictionary<NSString *,NSString *> which isn't expressive enough
> - many properties that don't universally make sense like
...
> - also NSURLRequest is also a concrete type so your points from above also apply there, right?

You misunderstood me. I’m saying that `HTTPRequest` should be a protocol and that Foundation.URLRequest could be one implementation conforming to it. And subsequently work with all APIs using the new S3 `HTTPRequest`.

>> - you should probably not add ‘convenience’ to this, that
>> will be done by frameworks using it in whatever they
>> consider ‘convenient’
> that was more for the benefit of the readers of this mailing list. But yes, in the actual API, I'd leave that comment out.

If it is a protocol I’d leave the whole method out! :-) More like:

  protocol Writer {
    func writeBody(data: DispatchData)
  }
  extension Writer { // convenience
    func writeBody(data: Data)

  }

>> - or it other words: does providing a `write` w/o a
>>   completion handler give performance benefits and
>>   is part of the API because of this, or is it just
>>   `write(..) { _ in }`. I could see that it can be a
>>   perf advantage (no setup of an escaping closure
>>   necessary), but then it should not be marked as
>>   ‘convenience’
>> - same for Data vs DispatchData. Decide on just one?
> 
> hmm, if you happen to have a DispatchData it does give you performance benefits to hand that down. So in our implementation I started off with only DispatchData but it then turned out that it's convenient to also being able to write data directly. Especially before we had our higher level web frameworks on top of that. But maybe that's not needed.

If Data doesn’t replace DispatchData eventually, I think the Foundation team should provide cheap ways to convert between the two. It is a little weird that there are two of those …


>> - maybe such:
>> 
>>    func writeTrailer(key: String, value: String)
>> 
>> should be
>> 
>>    func writeTrailer(key: UnsafePointer<CChar>,
>>                      value: UnsafePointer<CChar>)
>> 
>> this also works with strings out of the box while
>> allowing applications not using Strings for protocol
>> data ;->
> 
> will work with string _literals_ not Strings, maybe we should have both?

It works with any String:

    func printIt(_ key: UnsafePointer<CChar>) {
      let s = String(cString: key)
      print("key is \(s)”)
    }
    var abc = “hello”
    abc += “ world”
    printIt(abc)


>> - I guess all ‘write’ functions need a completion
>> callback? (including writeTrailer, writeResponse)
> 
> in my experience having that on the done() handler is enough. The amount of data written in trailers/response (headers)/... is small enough that you can do it regardless.

Maybe. Not sure, depends a little on the async framework in use. A header can certainly be big enough to overflow the socket buffer and block.
Nginx uses 40 bytes per connection? I’m sure they wouldn’t want to be forced to buffer a 700 byte header block :-)

Not sure, I guess you are right.


>> - I’m still a little worried about using `enum`s for
>> an evolving protocol. That will look nice now but
>> will fall apart in the future as we can’t extend
>> them w/o breaking apps
> 
> which of the enums will be necessary to evolve?

All of them. E.g. PATCH is pretty new, or MKCOLLECTION. If a new method or HTTP status becomes ubiquitous, it will quickly get weird, all code will look like:

  switch method {
    case .get: …
    case .put: ...
    case .delete: …
    case custom(“BATCH”): // wtf! so ugly
  }

Makes your pretty Swift code really ugly :-) Int’s plus constants are better for stuff that can change.


> Both have a `custom` case now for everything else. And most use-cases would use higher-level frameworks anyway.

Well, ideally a framework could reuse common stuff like the HTTPMethod type. It looks a little stupid to wrap such :-)


>> - Talking enums. You use them for status and method,
>> why not for header keys? I see _big_ performance
>> advantages in that.
> that's surprising, what exactly is giving you the big advantages and how much is big? Is it the storage or what exactly?

- storage (essentially a byte vs the full String thing)

- storage reuse (are you going to reconstruct the String
  every time you parse a header? or have a thread safe
  intern map?) No issue with a byte.

- ARC (potentially, much more likely than for Ints ;-)

- comparison speed - compare two bytes (a single
  assembly instruction) vs comparing two Strings,
  potentially unique ones with all the checks involved.
  Order of magnitude more assembly instructions (I guess ;-)

That is also a reason why I’m in favour of supporting value
types instead of just string. Converting your content-length
into a String is a little stupid. That can be done at the
C level below much faster.

As I said, I suppose people that need performance may be doing their own thing anyways. But it would be cool if it would be great out of the box :-)
BTW: I’m not necessarily thinking high-scale cloud servers, I’m thinking more about stuff running on Rasπ’s.


>> - Having the stream support ‘HTTPResponse’ looks like
>> too high level for your specific design. You can keep
>> that object and add a convenience method to put such
>> on the stream.
> 
> as all the others above, I think very good discussion points. Anyone else having opinions?
> 
> 
>> - also: HTTPResponse vs HTTPResponseHead[er?]
> 
> sure, why not? ;)


I’d like to mention again that in HTTP/2 methods are just headers. Maybe the stuff should be modelled after that?

  request[.method] == .put

hh

> 
> 
> Thanks,
>  -- Johannes
>> 
>> That is it for now :-)
>> 
>> hh
>> 
>>> On 5. Apr 2017, at 19:35, Johannes Weiß <johannesweiss at apple.com> wrote:
>>> 
>>> Hi,
>>> 
>>> First of all, thanks very much to Helge for carefully analysing (and implementing) the first API sketch. As promised, I reworked it a bit.
>>> 
>>> Changes:
>>> - extracted HTTPResponse as its own value type (so it could also be used for a client)
>>> - added Helge's suggestions:
>>> * proper backpressure support
>>> * being able to ignore further parts of the HTTP body (stop parameter)
>>> 
>>> If I forgot something important, please let me know. I can't promise that I'll address it before the meeting as I'm in another office tomorrow for meetings and probably won't be able to attend either.
>>> 
>>> Please find everything below...
>>> 
>>> Cheers,
>>> Johannes
>>> 
>>> --- SNIP ---
>>> /* a web app is a function that gets a HTTPRequest and a HTTPResponseWriter and returns a function which processes the HTTP request body in chunks as they arrive */
>>> 
>>> public typealias WebApp = (HTTPRequest, HTTPResponseWriter) -> HTTPBodyProcessing
>>> 
>>> public struct HTTPRequest {
>>> public var method : HTTPMethod
>>> public var target : String /* e.g. "/foo/bar?buz=qux" */
>>> public var httpVersion : HTTPVersion
>>> public var headers : HTTPHeaders
>>> }
>>> 
>>> public struct HTTPResponse {
>>> public var httpVersion : HTTPVersion
>>> public var status: HTTPResponseStatus
>>> public var transferEncoding: HTTPTransferEncoding
>>> public var headers: HTTPHeaders
>>> }
>>> 
>>> public protocol HTTPResponseWriter: class {
>>> func writeContinue(headers: HTTPHeaders = HTTPHeaders()) /* to send an HTTP `100 Continue` */
>>> 
>>> func writeResponse(_ response: HTTPResponse)
>>> 
>>> func writeTrailer(key: String, value: String)
>>> 
>>> func writeBody(data: DispatchData) /* convenience */
>>> func writeBody(data: Data) /* convenience */
>>> func writeBody(data: DispatchData, completion: @escaping (Result<POSIXError, ()>) -> Void)
>>> func writeBody(data: Data, completion: @escaping (Result<POSIXError, ()>) -> Void)
>>> 
>>> func done() /* convenience */
>>> func done(completion: @escaping (Result<POSIXError, ()>) -> Void)
>>> func abort()
>>> }
>>> 
>>> public typealias HTTPBodyHandler = (HTTPBodyChunk, inout Bool) -> Void /* the Bool can be set to true when we don't want to process anything further */
>>> 
>>> public enum HTTPBodyProcessing {
>>> case discardBody /* if you're not interested in the body */
>>> case processBody(handler: HTTPBodyHandler)
>>> }
>>> 
>>> public enum HTTPBodyChunk {
>>> case chunk(data: DispatchData, finishedProcessing: () -> Void) /* a new bit of the HTTP request body has arrived, finishedProcessing() must be called when done with that chunk */
>>> case failed(error: HTTPParserError) /* error while streaming the HTTP request body, eg. connection closed */
>>> case trailer(key: String, value: String) /* trailer has arrived (this we actually haven't implemented yet) */
>>> case end /* body and trailers finished */
>>> }
>>> 
>>> public struct HTTPHeaders {
>>> var storage: [String:[String]]     /* lower cased keys */
>>> var original: [(String, String)]   /* original casing */
>>> var description: String
>>> 
>>> subscript(key: String) -> [String]
>>> func makeIterator() -> IndexingIterator<Array<(String, String)>>
>>> 
>>> init(_ headers: [(String, String)] = [])
>>> }
>>> 
>>> public typealias HTTPVersion = (Int, Int)
>>> 
>>> public enum HTTPTransferEncoding {
>>> case identity(contentLength: UInt)
>>> case chunked
>>> }
>>> 
>>> public enum HTTPResponseStatus {
>>> /* use custom if you want to use a non-standard response code or
>>>   have it available in a (UInt, String) pair from a higher-level web framework. */
>>> case custom(code: UInt, reasonPhrase: String)
>>> 
>>> /* all the codes from http://www.iana.org/assignments/http-status-codes */
>>> case `continue`
>>> case switchingProtocols
>>> case processing
>>> case ok
>>> case created
>>> case accepted
>>> case nonAuthoritativeInformation
>>> case noContent
>>> case resetContent
>>> case partialContent
>>> case multiStatus
>>> case alreadyReported
>>> case imUsed
>>> case multipleChoices
>>> case movedPermanently
>>> case found
>>> case seeOther
>>> case notModified
>>> case useProxy
>>> case temporaryRedirect
>>> case permanentRedirect
>>> case badRequest
>>> case unauthorized
>>> case paymentRequired
>>> case forbidden
>>> case notFound
>>> case methodNotAllowed
>>> case notAcceptable
>>> case proxyAuthenticationRequired
>>> case requestTimeout
>>> case conflict
>>> case gone
>>> case lengthRequired
>>> case preconditionFailed
>>> case payloadTooLarge
>>> case uriTooLong
>>> case unsupportedMediaType
>>> case rangeNotSatisfiable
>>> case expectationFailed
>>> case misdirectedRequest
>>> case unprocessableEntity
>>> case locked
>>> case failedDependency
>>> case upgradeRequired
>>> case preconditionRequired
>>> case tooManyRequests
>>> case requestHeaderFieldsTooLarge
>>> case unavailableForLegalReasons
>>> case internalServerError
>>> case notImplemented
>>> case badGateway
>>> case serviceUnavailable
>>> case gatewayTimeout
>>> case httpVersionNotSupported
>>> case variantAlsoNegotiates
>>> case insufficientStorage
>>> case loopDetected
>>> case notExtended
>>> case networkAuthenticationRequired
>>> }
>>> 
>>> public enum HTTPMethod {
>>> case custom(method: String)
>>> 
>>> /* everything that http_parser.[ch] supports */
>>> case DELETE
>>> case GET
>>> case HEAD
>>> case POST
>>> case PUT
>>> case CONNECT
>>> case OPTIONS
>>> case TRACE
>>> case COPY
>>> case LOCK
>>> case MKCOL
>>> case MOVE
>>> case PROPFIND
>>> case PROPPATCH
>>> case SEARCH
>>> case UNLOCK
>>> case BIND
>>> case REBIND
>>> case UNBIND
>>> case ACL
>>> case REPORT
>>> case MKACTIVITY
>>> case CHECKOUT
>>> case MERGE
>>> case MSEARCH
>>> case NOTIFY
>>> case SUBSCRIBE
>>> case UNSUBSCRIBE
>>> case PATCH
>>> case PURGE
>>> case MKCALENDAR
>>> case LINK
>>> case UNLINK
>>> }
>>> --- SNAP ---
>>> 
>>> Here's the demo code for a simple echo server
>>> 
>>> --- SNIP ---
>>> serve { (req, res) in
>>> if req.target == "/echo" {
>>>     guard req.httpVersion == (1, 1) else {
>>>         /* HTTP/1.0 doesn't support chunked encoding */
>>>         res.writeResponse(HTTPResponse(version: req.version,
>>>                                        status: .httpVersionNotSupported,
>>>                                        transferEncoding: .identity(contentLength: 0)))
>>>         res.done()
>>>         return .discardBody
>>>     }
>>>     res.writeResponse(HTTPResponse(version: req.version,
>>>                                    status: .ok,
>>>                                    transferEncoding: .chunked,
>>>                                    headers: SomeConcreteHTTPHeaders([("X-foo": "bar")])))
>>>     return .processBody { (chunk, stop) in
>>>         switch chunk {
>>>             case .chunk(let data, let finishedProcessing):
>>>                 res.writeBody(data: data) { _ in
>>>                     finishedProcessing()
>>>                 }
>>>             case .end:
>>>                 res.done()
>>>             default:
>>>                 stop = true /* don't call us anymore */
>>>                 res.abort()
>>>         }
>>>     }
>>> } else { ... }
>>> }
>>> --- SNAP ---

-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 842 bytes
Desc: Message signed with OpenPGP using GPGMail
URL: <https://lists.swift.org/pipermail/swift-server-dev/attachments/20170407/c6793f72/attachment.sig>


More information about the swift-server-dev mailing list