mirror of https://github.com/vapor/docs.git
283 lines
8.7 KiB
Markdown
283 lines
8.7 KiB
Markdown
# Content
|
|
|
|
Vapor's content API allows you to easily encode / decode Codable structs to / from HTTP messages. [JSON](https://tools.ietf.org/html/rfc7159) encoding is used by default with out-of-the-box support for [URL-Encoded Form](https://en.wikipedia.org/wiki/Percent-encoding#The_application/x-www-form-urlencoded_type) and [Multipart](https://tools.ietf.org/html/rfc2388). The API is also configurable, allowing for you to add, modify, or replace encoding strategies for certain HTTP content types.
|
|
|
|
## Overview
|
|
|
|
To understand how Vapor's content API works, you should first understand a few basics about HTTP messages. Take a look at the following example request.
|
|
|
|
```http
|
|
POST /greeting HTTP/1.1
|
|
content-type: application/json
|
|
content-length: 18
|
|
|
|
```
|
|
|
|
This request indicates that it contains JSON-encoded data using the `content-type` header and `application/json` media type. As promised, some JSON data follows after the headers in the body.
|
|
|
|
### Content Struct
|
|
|
|
The first step to decoding this HTTP message is creating a Codable type that matches the expected structure.
|
|
|
|
```swift
|
|
struct Greeting: Content {
|
|
var hello: String
|
|
}
|
|
```
|
|
|
|
Conforming the type to `Content` will automatically add conformance to `Codable` alongside additional utilities for working with the content API.
|
|
|
|
Once you have the content structure, you can decode it from the incoming request using `req.content`.
|
|
|
|
```swift
|
|
app.post("greeting") { req in
|
|
let greeting = try req.content.decode(Greeting.self)
|
|
print(greeting.hello) // "world"
|
|
return HTTPStatus.ok
|
|
}
|
|
```
|
|
|
|
The decode method uses the request's content type to find an appropriate decoder. If there is no decoder found, or the request does not contain the content type header, a `415` error will be thrown.
|
|
|
|
That means that this route automatically accepts all of the other supported content types, such as url-encoded form:
|
|
|
|
```http
|
|
POST /greeting HTTP/1.1
|
|
content-type: application/x-www-form-urlencoded
|
|
content-length: 11
|
|
|
|
hello=world
|
|
```
|
|
|
|
In the case of file uploads, your content property must be of type `Data`
|
|
|
|
```swift
|
|
struct Profile: Content {
|
|
var name: String
|
|
var email: String
|
|
var image: Data
|
|
}
|
|
```
|
|
|
|
### Supported Media Types
|
|
|
|
Below are the media types the content API supports by default.
|
|
|
|
|name|header value|media type|
|
|
|-|-|-|
|
|
|JSON|application/json|`.json`|
|
|
|Multipart|multipart/form-data|`.formData`|
|
|
|URL-Encoded Form|application/x-www-form-urlencoded|`.urlEncodedForm`|
|
|
|Plaintext|text/plain|`.plainText`|
|
|
|HTML|text/html|`.html`|
|
|
|
|
Not all media types support all `Codable` features. For example, JSON does not support top-level fragments and Plaintext does not support nested data.
|
|
|
|
## Query
|
|
|
|
Vapor's Content APIs support handling URL encoded data in the URL's query string.
|
|
|
|
### Decoding
|
|
|
|
To understand how decoding a URL query string works, take a look at the following example request.
|
|
|
|
```http
|
|
GET /hello?name=Vapor HTTP/1.1
|
|
content-length: 0
|
|
```
|
|
|
|
Just like the APIs for handling HTTP message body content, the first step for parsing URL query strings is to create a `struct` that matches the expected structure.
|
|
|
|
```swift
|
|
struct Hello: Content {
|
|
var name: String?
|
|
}
|
|
```
|
|
|
|
Note that `name` is an optional `String` since URL query strings should always be optional. If you want to require a parameter, use a route parameter instead.
|
|
|
|
Now that you have a `Content` struct for this route's expected query string, you can decode it.
|
|
|
|
```swift
|
|
app.get("hello") { req -> String in
|
|
let hello = try req.query.decode(Hello.self)
|
|
return "Hello, \(hello.name ?? "Anonymous")"
|
|
}
|
|
```
|
|
|
|
This route would result in the following response given the example request from above:
|
|
|
|
```http
|
|
HTTP/1.1 200 OK
|
|
content-length: 12
|
|
|
|
Hello, Vapor
|
|
```
|
|
|
|
If the query string were omitted, like in the following request, the name "Anonymous" would be used instead.
|
|
|
|
```http
|
|
GET /hello HTTP/1.1
|
|
content-length: 0
|
|
```
|
|
|
|
### Single Value
|
|
|
|
In addition to decoding to a `Content` struct, Vapor also supports fetching single values from the query string using subscripts.
|
|
|
|
```swift
|
|
let name: String? = req.query["name"]
|
|
```
|
|
|
|
## Hooks
|
|
|
|
Vapor will automatically call `beforeEncode` and `afterDecode` on a `Content` type. Default implementations are provided which do nothing, but you can use these methods to run custom logic.
|
|
|
|
```swift
|
|
// Runs after this Content is decoded. `mutating` is only required for structs, not classes.
|
|
mutating func afterDecode() throws {
|
|
// Name may not be passed in, but if it is, then it can't be an empty string.
|
|
self.name = self.name?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if let name = self.name, name.isEmpty {
|
|
throw Abort(.badRequest, reason: "Name must not be empty.")
|
|
}
|
|
}
|
|
|
|
// Runs before this Content is encoded. `mutating` is only required for structs, not classes.
|
|
mutating func beforeEncode() throws {
|
|
// Have to *always* pass a name back, and it can't be an empty string.
|
|
guard
|
|
let name = self.name?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!name.isEmpty
|
|
else {
|
|
throw Abort(.badRequest, reason: "Name must not be empty.")
|
|
}
|
|
self.name = name
|
|
}
|
|
```
|
|
|
|
## Override Defaults
|
|
|
|
The default encoders and decoders used by Vapor's Content APIs can be configured.
|
|
|
|
### Global
|
|
|
|
`ContentConfiguration.global` lets you change the encoders and decoders Vapor uses by default. This is useful for changing how your entire application parses and serializes data.
|
|
|
|
```swift
|
|
// create a new JSON encoder that uses unix-timestamp dates
|
|
let encoder = JSONEncoder()
|
|
encoder.dateEncodingStrategy = .secondsSince1970
|
|
|
|
// override the global encoder used for the `.json` media type
|
|
ContentConfiguration.global.use(encoder: encoder, for: .json)
|
|
```
|
|
|
|
Mutating `ContentConfiguration` is usually done in `configure.swift`.
|
|
|
|
### One-Off
|
|
|
|
Calls to encoding and decoding methods like `req.content.decode` support passing in custom coders for one-off usages.
|
|
|
|
```swift
|
|
// create a new JSON decoder that uses unix-timestamp dates
|
|
let decoder = JSONDecoder()
|
|
decoder.dateDecodingStrategy = .secondsSince1970
|
|
|
|
// decodes Hello struct using custom decoder
|
|
let hello = try req.content.decode(Hello.self, using: decoder)
|
|
```
|
|
|
|
## Custom Coders
|
|
|
|
Applications and third-party packages can add support for media types that Vapor does not support by default by creating custom coders.
|
|
|
|
### Content
|
|
|
|
Vapor specifies two protocols for coders capable of handling content in HTTP message bodies: `ContentDecoder` and `ContentEncoder`.
|
|
|
|
```swift
|
|
public protocol ContentEncoder {
|
|
func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws
|
|
where E: Encodable
|
|
}
|
|
|
|
public protocol ContentDecoder {
|
|
func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D
|
|
where D: Decodable
|
|
}
|
|
```
|
|
|
|
Conforming to these protocols allows your custom coders to be registered to `ContentConfiguration` as specified above.
|
|
|
|
### URL Query
|
|
|
|
Vapor specifies two protocols for coders capable of handling content in URL query strings: `URLQueryDecoder` and `URLQueryEncoder`.
|
|
|
|
```swift
|
|
public protocol URLQueryDecoder {
|
|
func decode<D>(_ decodable: D.Type, from url: URI) throws -> D
|
|
where D: Decodable
|
|
}
|
|
|
|
public protocol URLQueryEncoder {
|
|
func encode<E>(_ encodable: E, to url: inout URI) throws
|
|
where E: Encodable
|
|
}
|
|
```
|
|
|
|
Conforming to these protocols allows your custom coders to be registered to `ContentConfiguration` for handling URL query strings using the `use(urlEncoder:)` and `use(urlDecoder:)` methods.
|
|
|
|
### Custom `ResponseEncodable`
|
|
|
|
Another approach involves implementing `ResponseEncodable` on your types. Consider this trivial `HTML` wrapper type:
|
|
|
|
```swift
|
|
struct HTML {
|
|
let value: String
|
|
}
|
|
```
|
|
|
|
Then its `ResponseEncodable` implementation would look like this:
|
|
|
|
```swift
|
|
extension HTML: ResponseEncodable {
|
|
public func encodeResponse(for request: Request) -> EventLoopFuture<Response> {
|
|
var headers = HTTPHeaders()
|
|
headers.add(name: .contentType, value: "text/html")
|
|
return request.eventLoop.makeSucceededFuture(.init(
|
|
status: .ok, headers: headers, body: .init(string: value)
|
|
))
|
|
}
|
|
}
|
|
```
|
|
|
|
If you're using `async`/`await` you can use `AsyncResponseEncodable`:
|
|
|
|
```swift
|
|
extension HTML: AsyncResponseEncodable {
|
|
public func encodeResponse(for request: Request) async throws -> Response {
|
|
var headers = HTTPHeaders()
|
|
headers.add(name: .contentType, value: "text/html")
|
|
return .init(status: .ok, headers: headers, body: .init(string: value))
|
|
}
|
|
}
|
|
```
|
|
|
|
Note that this allows customizing the `Content-Type` header. See [`HTTPHeaders` reference](https://api.vapor.codes/vapor/main/Vapor/) for more details.
|
|
|
|
You can then use `HTML` as a response type in your routes:
|
|
|
|
```swift
|
|
app.get { _ in
|
|
HTML(value: """
|
|
<html>
|
|
<body>
|
|
<h1>Hello, World!</h1>
|
|
</body>
|
|
</html>
|
|
""")
|
|
}
|
|
```
|