[swift-server-dev] HTTP API Sketch v2

Colin Barrett colin at springsandstruts.com
Thu Apr 6 16:20:49 CDT 2017

On Thu, Apr 6, 2017 at 3:53 AM Johannes Weiß <johannesweiss at apple.com>

Hi Colin,

Is writing compositional middleware a goal for this iteration of the API? I
noticed that the HTTPResponseWriter doesn't appear to allow for, e.g.
writing a single header and then passing the writer on (as one would do in,
say, an anti-CSRF middleware). Possible I'm missing something though!

Good point! The version 1 actually only had writeHeader(key:value:) and for
the sake of having a HTTPResponse type (that could then be used by an HTTP
client) I got rid of that. But maybe that was over the top and we should go
back to what I had before.

I think having the HTTPResponse is nice. I would suggest these changes:

writeResponse(..., finishHeaders = true)
finishHeaders() [-> Bool] // It is an error to call writeHeader after
finishHeaders. Calling finishHeaders twice is a no-op. (Return value could
differentiates those cases?)
writeBody(...) // Calls finishHeaders()

Sorry for the brevity of my sketch, let me know if that's unclear in any




On Wed, Apr 5, 2017 at 1:36 PM Johannes Weiß via swift-server-dev <
swift-server-dev at swift.org> wrote:


First of all, thanks very much to Helge for carefully analysing (and
implementing) the first API sketch. As promised, I reworked it a bit.

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


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

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 SEARCH
  case UNLOCK
  case BIND
  case REBIND
  case UNBIND
  case ACL
  case REPORT
  case MERGE
  case MSEARCH
  case NOTIFY
  case PATCH
  case PURGE
  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,
.identity(contentLength: 0)))
           return .discardBody
       res.writeResponse(HTTPResponse(version: req.version,
                                      status: .ok,
                                      transferEncoding: .chunked,
SomeConcreteHTTPHeaders([("X-foo": "bar")])))
       return .processBody { (chunk, stop) in
           switch chunk {
               case .chunk(let data, let finishedProcessing):
                   res.writeBody(data: data) { _ in
               case .end:
                   stop = true /* don't call us anymore */
   } else { ... }
--- SNAP ---

swift-server-dev mailing list
swift-server-dev at swift.org
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-server-dev/attachments/20170406/b97c2b0b/attachment.html>

More information about the swift-server-dev mailing list