[swift-server-dev] HTTP API Sketch v2

Colin Barrett colin at springsandstruts.com
Fri Apr 7 15:54:46 CDT 2017


On Fri, Apr 7, 2017 at 6:01 AM Johannes Weiß <johannesweiss at apple.com>
wrote:

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

Mostly just for smell reasons. Calls to writeHeader are going to fail after
writeBody. This can be seen as an additional side effect of writeBody,
besides actually emitting the body. Having two entangled effects like this
usually leads regret, or people abusing the API.

Just to be clear, its fine for one method (writeBody) to invoke the other
(finishHeaders), but users should be able to control the timing of each
effect should they so choose (for example to help enforce invariants in
their own code—"past this point I promise to write no more headers").

It's something that could be done in a wrapper class, so it's not the end
of the world if you decide to leave it out.

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
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-server-dev/attachments/20170407/dcfedd5b/attachment.html>


More information about the swift-server-dev mailing list