[swift-server-dev] HTTP API Sketch v2

Johannes WeiƟ johannesweiss at apple.com
Wed Apr 5 12:35:08 CDT 2017


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