[swift-server-dev] HTTP API Sketch v2

Helge Heß me at helgehess.eu
Thu Apr 6 03:28:39 CDT 2017


Hi Johannes,

my notes on it:

- concrete types instead of protocols for HTTPRequest
  - bad for layering this on top of alternative
    implementations
  - bad because it prohibits future changes in the
    implementation (e.g. I still think it is a bad idea
    to convert everything to Strings in advance)
  - it can still come with a default imp
  - also: you could make NSURLRequest/Response support it

- maybe HTTPRequest should be called HTTPRequestHead,
  because in this case it really is what it is, it
  doesn’t even have a stream reference attached to it.

- you should probably not add ‘convenience’ to this, that
  will be done by frameworks using it in whatever they
  consider ‘convenient’
  - 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?

- 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 ;->

- I guess all ‘write’ functions need a completion 
  callback? (including writeTrailer, writeResponse)

- 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

- Talking enums. You use them for status and method,
  why not for header keys? I see _big_ performance
  advantages in that.

- 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.
  - also: HTTPResponse vs HTTPResponseHead[er?]

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 ---
> 



More information about the swift-server-dev mailing list