[swift-server-dev] HTTP API Sketch v2

Johannes Weiß johannesweiss at apple.com
Fri Apr 7 05:01:26 CDT 2017


Hi Colin,

> On 6 Apr 2017, at 22:20, Colin Barrett <colin at springsandstruts.com> wrote:
> 
> On Thu, Apr 6, 2017 at 3:53 AM Johannes Weiß <johannesweiss at apple.com> wrote:
> 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)
> writeHeader(...)
> 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 way.

Thanks, that makes sense! I literally just started an API Sketch v3 document where I put these in so I won't forget.

Just one question about the finishHeaders(): You see this as being mandatory to call, right? At the moment our implementation doesn't have that and it's handled implicitly. Basically if writeBody/writeTrailer/done are called we finish the headers if that hasn't been done yet. In other words, there's a state machine in HTTPResponseWriter which has a headers finished state but it's managed internally.

Do you see any advantages of exposing that explicitly?

Cheers,
  Johannes

> 
> Thanks,
> -Colin
> 
>  
> -- 
>   Johannes
> 
>> 
>> -Colin
>> 
>> On Wed, Apr 5, 2017 at 1:36 PM Johannes Weiß via swift-server-dev <swift-server-dev at swift.org> 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 ---
>> 
>> _______________________________________________
>> swift-server-dev mailing list
>> swift-server-dev at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-server-dev



More information about the swift-server-dev mailing list