Skip to content

PerfectlySoft/Perfect-HTTP

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Get Involed with Perfect!

Star Perfect On Github Stack Overflow Follow Perfect on Twitter Join the Perfect Slack

Swift 4.0 Platforms OS X | Linux License Apache PerfectlySoft Twitter Slack Status

Base HTTP Support for Perfect

This repository is a dependency for the Perfect HTTP/1, HTTP/2, and FastCGI servers. Please look at Perfect HTTPServer for more details.

Overview

The HTTP library provides a set of Enums, Structs, Protocols and functions to handle interactions with HTTP clients. It provides concrete implimentations for the URL routing mechanism. When you are setting up an HTTPServer you will need to import this library to use the Routing functions. Generally:

import PerfectHTTP
import PerfectHTTPServer

An example of a routing declaration using a closure block as the handler:

var routes = Routes()
routes.add(method: .get, uri: "/") {
	request, response in
	response.appendBody(string: "<html><title>Hello, world!</title><body>Hello, world!</body></html>")
	response.completed()
}

The handler can be a separate function which takes an HTTPRequest, an HTTPResponse and either completes the response or hands off to a function which does.

func helloWorld(request: HTTPRequest, response: HTTPResponse) {
	response.appendBody(string: "<html><title>Hello, world!</title><body>Hello, world!</body></html>")
		.completed()
}
routes.add(method: .get, uri: "/hello", handler: helloWorld)

The routes must be added to the server instance before it is started.

try HTTPServer.launch(name: "my.server.ca", port: port, routes: routes)

Routes can not be added or modified after a server has started listening for requests.

Inside your handler function the HTTPRequest object provides access to all client request information. This includes all client headers, query parameters, POST body data and other relevant information such as the client IP address and URL variables.

HTTPRequest will handle parsing and decoding all "application/x-www-form-urlencoded" as well as "multipart/form-data" content type requests. It will make the data for any other content types available in a raw, unparsed form. When handling multipart form data, HTTPRequest will automatically decode the data and create temporary files for any file uploads contained therein. These files will exist until the request ends after which they will be automatically deleted.

As you build the return values, the HTTPResponse object contains all outgoing response data. This consists of the HTTP status code and message, the HTTP headers, and any response body data. HTTPResponse also contains the ability to stream or push chunks of response data to the client and to complete or terminate the request.

Typed Routing

In addition to raw handlers which accept request and response objects, Perfect-HTTP routes also support strongly typed handlers which decode, accept, or return Codable Swift objects.

The APIs for working with typed routes are very similar to those for working with un-typed routes. The objects for producing typed routes are named TRoutes and TRoute. The meaning and usage of these objects correspond closely to those of the Routes and Route objects, respectively.

The interfaces for these objects are as follows:

/// A typed intermediate route handler parameterized on the input and output types.
public struct TRoutes<I, O> {
	/// Input type alias
	public typealias InputType = I
	/// Output type alias
	public typealias OutputType = O
	/// Init with a base URI and handler.
	public init(
		baseUri u: String,
		handler t: @escaping (InputType) throws -> OutputType)
	/// Add a typed route to this base URI.
	@discardableResult
	public mutating func add<N>(_ route: TRoute<OutputType, N>) -> TRoutes
	/// Add other intermediate routes to this base URI.
	@discardableResult
	public mutating func add<N>(_ route: TRoutes<OutputType, N>) -> TRoutes
	/// Add a route to this object. The new route will take the output of this route as its input.
	@discardableResult
	public mutating func add<N: Codable>(
		method m: HTTPMethod,
		uri u: String,
		handler t: @escaping (OutputType) throws -> N) -> TRoutes
}

/// A typed route handler.
public struct TRoute<I, O: Codable> {
	/// Input type alias.
	public typealias InputType = I
	/// Output type alias.
	public typealias OutputType = O
	// Init with a method, uri, and handler.
	public init(
		method m: HTTPMethod,
		uri u: String,
		handler t: @escaping (InputType) throws -> OutputType)
	/// Init with zero or more methods, a uri, and handler.
	public init(
		methods m: [HTTPMethod] = [.get, .post],
		uri u: String,
		handler t: @escaping (InputType) throws -> OutputType)
}

Just as with Routes and Route objects, a TRoutes is an intermediate handler and a TRoute is a terminal handler.

A TRoutes handler can be created accepting either an HTTPRequest object or any other type of object which may be passed down from a previous handler. The first TRoutes handler is usually created accepting the HTTPRequest object. This handler in turn processes its input and returns some object which is given to subsequent handlers.

A TRoute handler accepts some sort of input type and returns a Codable object. This codable object is serialized to JSON and returned to the client.

The input to both types of handlers will either be the HTTPRequest, the result of decoding the request body to some Decodable object, or the return value of whatever intermediate handler occurred immediately before. When a handler is defined as receiving a Decodable object, the HTTPRequest body will be automatically decoded into this type. If the body can not be decoded then an Error will be thrown and the error response will be returned to the client. Alternatively, a handler can be defined as accepting the HTTPRequest object but can decode the body itself using the HTTPRequest.decode function (described below).

Request Body Decode

Two extensions on the HTTPRequest object aid in decoding the request body.

/// Extensions on HTTPRequest which permit the request body to be decoded to a Codable type.
public extension HTTPRequest {
	/// Decode the request as the desired object.
	func decode<A: Codable>() throws -> A
	/// Identity decode. Used to permit generic code to operate with the HTTPRequest
	func decode() throws -> Self
}

The first function will decode the body into the desired Codable object. If the request's content-type is application/json then the body will be decoded from that JSON. Otherwise, the request's URL encoded GET or POST arguments will be used for the decode. Additionally, any URL variables (described later in this document) will be utilized for the decode. This allows for a mixture of GET/POST arguments and URL variables to be brought together when decoding the object.

Note that when decoding objects from non-JSON request data, nested, non-integral objects are not supported. Objects with array properties are also not supported in this case.

Response Error

If either an intermediate or terminal typed handler experiences an error during processing, they can throw an HTTPResponseError. Initializing one of these objects requires both an HTTPResponseStatus and a String description of the problem.

/// A codable response type indicating an error.
public struct HTTPResponseError: Error, Codable, CustomStringConvertible {
	/// The HTTP status for the response.
	public let status: HTTPResponseStatus
	/// Textual description of the error.
	public let description: String
	/// Init with status and description.
	public init(
		status s: HTTPResponseStatus,
		description d: String)
}

Support Extensions

Extensions on Routes permits adding TRoutes or TRoute objects.

public extension Routes {
	/// Add routes to this object.
	mutating func add<I, O>(_ route: TRoutes<I, O>)
	/// Add a route to this object.
	mutating func add<I, O>(_ route: TRoute<I, O>)
}

Typed Routing Examples

The following example shows how Codable objects for a route would be defined and how the typed routes would be added to a Routes object.

In this abbreviated example the intermediate handler for "/api" would perform some screening on the request to ensure the client has been authenticated. It would then return to the next handler (which is terminal, in this case) a tuple consisting of the original HTTPRequest object as well as a SessionInfo object containing whatever client id had been pulled from the request. The terminal handler "/api/info/{id}" would then use this information to complete the request and return the response.

struct SessionInfo: Codable {
	//...could be an authentication token, etc.
	let id: String
}
struct RequestResponse: Codable {
	struct Address: Codable {
		let street: String
		let city: String
		let province: String
		let country: String
		let postalCode: String
	}
	let fullName: String
	let address: Address
}
// when handlers further down need the request you can pass it along. this is not necessary though
typealias RequestSession = (request: HTTPRequest, session: SessionInfo)

// intermediate handler for /api
func checkSession(request: HTTPRequest) throws -> RequestSession {
	// one would check the request to make sure it's authorized
	let sessionInfo: SessionInfo = try request.decode() // will throw if request does not include id
	return (request, sessionInfo)
}

// terminal handler for /api/info/{id}
func userInfo(session: RequestSession) throws -> RequestResponse {
	// return the response for this request
	return .init(fullName: "Justin Trudeau",
		address: .init(
			street: "111 Wellington St",
			city: "Ottawa",
			province: "Ontario",
			country: "Canada",
			postalCode: "K1A 0A6"))
}
// root Routes object holding all other routes for this server
var routes = Routes()
// types routes object for the /api URI
var apiRoutes = TRoutes(baseUri: "/api", handler: checkSession)
// add terminal handler for the /info/{id} URI suffix
apiRoutes.add(method: .get, uri: "/info/{id}", handler: userInfo)
// add the typed routes to the root
routes.add(apiRoutes)

// add routes to server and launch
try HTTPServer.launch(name: "my.server.ca", port: port, routes: routes)

More Information

The following documents contain pertinent information:

Configuring and Launching HTTPServer

Routing

All Perfect Docs