跳转至

序列化请求和响应实体

在Aqueduct中,HTTP请求和响应是RequestResponse的实例。 对于应用程序收到的每个HTTP请求,都会创建一个Request实例。 必须为每个请求创建一个Response。响应是由controller对象创建的。本指南讨论了请求和响应对象的行为:

请求对象

Request是为每个HTTP请求创建的。一个 Request存储了关于HTTP请求的一切,并且有一些额外的行为,可以让你更容易从它们中读取。你可以通过在controller对象或闭包中编写代码来处理请求。

一个请求的所有属性都可以在它的raw属性(Dart标准库HttpRequest)中获得。Request具有attachments ,可以将数据附加到控制器中以供链接的控制器使用:

router.route("/path").linkFunction((req) {
  req.attachments["key"] = "value";
}).linkFunction((req) {
  return Response.ok({"key": req.attachments["value"]});
});

Request还具有两个内置附件,即authorizationpathauthorization包含来自Authorizer的授权信息,path具有来自Router的请求路径信息。

响应对象

Response 具有状态码,标头和正文。 默认构造函数需要状态码,标头map和主体对象。 常见的响应类型有很多命名的构造函数:

Response(200, {"x-header": "value"}, body: [1, 2, 3]);
Response.ok({"key": "value"});
Response.created();
Response.badRequest(body: {"error": "reason"});

标头是根据dart:io.HttpHeaders.add进行编码的。关于正文的编码行为,请参阅下面的章节。

HTTP正文的编码和解码

Request and Response objects have behavior for handling the HTTP body. You decode the contents of a Request body into Dart objects that are used in your code. You provide a Dart object to a Response and it is automatically encoded according to the content-type of the response.

解码请求体

Every Request has a body property. This object decodes the bytes from the request body into Dart objects. The behavior for decoding is determined by the content-type header of the request (see the section on CodecRegistry later in this guide). When you decode a body, you can specify the Dart object type you expect it to be. If the decoded body object is not the expected type, an exception that sends a 400 Bad Request error is thrown.

// Ensures that the decoded body is a Map<String, dynamic>
final map = await request.body.decode<Map<String, dynamic>>();

// Takes whatever object the body is decoded into
final anyObject = await request.body.decode();

Once a request's body has been decoded, it can be accessed through a synchronous as method. This method also takes a type argument to enforce the type of the decoded body object.

final map = request.body.as<Map<String, dynamic>>();

Inferred Types

You don't need to provide a type argument to as or decode if the type can be inferred. For example, object.read(await request.body.decode()) will infer the type of the decoded body as a Map<String, dynamic> without having to provide type parameters.

If a body cannot be decoded according to its content-type (the data is malformed), an error is thrown that sends the appropriate error response to the client.

For more request body behavior, see the API reference for RequestBody, the section on body binding for ResourceControllers and a later section in this guide on Serializable.

Max Body Size

The size of a request body is limited to 10MB by default and can be changed by setting the value of RequestBody.maxSize during application initialization.

编码响应主体对象

HTTP响应通常包含 body 。 例如,响应GET /users/1的主体可能是代表用户的JSON对象。 为了确保客户端理解主体是JSON对象,它包含标头 Content-Type: application/json; charset=utf-8

创建带有主体的Response时,您需要提供一个body object和一个contentType。 例如:

var map = {"key": "value"};

// ContentType.json 是默认设置,可以省略设置。
// ContentType.json == `application/json; charset=utf-8'
final response = Response.ok(map)
  ..contentType = ContentType.json;

Body对象根据其内容类型进行编码。在上面的例子中,map首先被编码为JSON字符串,然后编码为UTF8字节的列表。

Map Encoding

ContentType由三个部分组成:主类型、子类型和可选字符集。

Content Type Components

主类型和子类型确定第一个转换步骤,字符集确定下一个转换步骤。 每个步骤都由Codec实例执行(来自dart:convert)。 例如,内容类型application/json选择JsonCodec,而字符集utf-8选择Utf8Codec。 连续运行这两个编解码器,以将Map转换为字节列表。 编解码器由应用程序的CodecRegistry选择; 这将在后面的部分中介绍。

主体对象必须对所选编解码器有效。 在上面的示例中,Map <String,dynamic>可以由JsonCodec编码。 但是,如果无法对主体对象进行编码,则会发送500服务器错误响应。 一个Codec的有效输入可能对另一个无效。 您需要确保body对象对于响应的contentType有效。

不是所有的内容类型都需要两个转换步骤。例如,当服务于一个HTML文件时,主体对象已经是一个HTML String。它只需要通过字符集编码器进行转换。

var html = "<html></html>";
var response = Response.ok(html)
  ..contentType = ContentType.html;

并且图像主体对象根本不需要转换,因为它已经是字节列表。 如果没有针对内容类型的已注册编解码器,则主体对象必须是字节数组(List <int>,其中每个值在0-255之间)。

final imageFile = File("image.jpg");
final imageBytes = await imageFile.readAsBytes();
final response = Response.ok(imageBytes)
  ..contentType = ContentType("image", "jpeg");

只要主体对象是一个字节数组,就可以禁止对主体进行自动编码:

final jsonBytes = utf8.encode(json.encode({"key": "value"}));
final response = Response.ok(jsonBytes)..encodeBody = false;

有关内容类型到编解码器映射的更多详细信息,请参见下一部分。 另外,请参阅CodecRegistry的文档,以获取有关内置编解码器和添加编解码器的详细信息。

流响应主体

A body object may also be a Stream<T>. Stream<T> body objects are most often used when serving files. This allows the contents of the file to be streamed from disk to the HTTP client without having to load the whole file into memory first. (See also FileController.)

final imageFile = File("image.jpg");
final imageByteStream = imageFile.openRead();
final response = new Response.ok(imageByteStream)
  ..contentType = new ContentType("image", "jpeg");

When a body object is a Stream<T>, the response will not be sent until the stream is closed. For finite streams - like those from opened filed - this happens as soon as the entire file is read. For streams that you construct yourself, you must close the stream some time after the response has been returned.

编解码器和内容类型

In the above sections, we glossed over how a codec gets selected when preparing the response body. The common case of ManagedObject<T> body objects that are sent as UTF8 encoded JSON 'just works' and is suitable for most applications. When serving assets for a web application or different data formats like XML, it becomes important to understand how Aqueduct's codec registry works.

CodecRegistry contains mappings from content types to Codecs. These codecs encode response bodies and decode request bodies. There are three built-in codecs for application/json, application/x-www-form-urlencoded and text/*. When a response is being sent, the repository is searched for an entry that exactly matches the primary and subtype of the Response.contentType. If an entry exists, the associated Codec starts the conversion. For example, if the content type is application/json; charset=utf-8, the built-in application/json codec encodes the body object.

If there isn't an exact match, but there is an entry for the primary type with the wildcard (*) subtype, that codec is used. For example, the built-in codec for text/* will be selected for both text/plain and text/html. If there was something special that had to be done for text/html, a more specific codec may be added for that type:

class MyChannel extends ApplicationChannel {
  @override
  Future prepare() async {
    CodecRegistry.defaultInstance.add(ContentType("application", "html"), HTMLCodec());
  }
}

Codecs must be added in your ApplicationChannel.prepare method. The codec must implement Codec from dart:convert. In the above example, when a response's content type is text/html, the HTMLCodec will encode the body object. This codec takes precedence over text/* because it is more specific.

When selecting a codec for a response body, the ContentType.charset doesn't impact which codec is selected. If a response's content-type has a charset, then a charset encoder like UTF8 will be applied as a last encoding step. For example, a response with content-type application/json; charset=utf-8 will encode the body object as a JSON string, which is then encoded as a list of UTF8 bytes. It is required that a response body's eventually encoded type is a list of bytes, so it follows that a codec that produces a string must have a charset.

If there is no codec in the repository for the content type of a Response, the body object must be a List<int> or Stream<List<int>>. If you find yourself converting data prior to setting it as a body object, it may make sense to add your own codec to CodecRegistry.

A request's body always starts as a list of bytes and is decoded into Dart objects. To decode a JSON request body, it first must be decoded from the list of UTF8 bytes into a string. It is possible that a client could omit the charset in its content-type header. Codecs added to CodecRegistry may specify a default charset to interpret a charset-less content-type. When a codec is added to the repository, if content-type's charset is non-null, that is the default. For example, the JSON codec is added like this:

CodecRegistry.defaultInstance.add(
  ContentType("application", "json", charset: "utf-8"),
  const JsonCodec(),
  allowCompression: true);

If no charset is specified when registering a codec, no charset decoding occurs on a request body if one doesn't exist. Content-types that are decoded from a String should not use a default charset because the repository would always attempt to decode the body as a string first.

用gzip压缩

Body objects may be compressed with gzip if the HTTP client allows it and the CodecRegistry has been configured to compress the content type of the response. The three built-in codecs - application/json, application/x-www-form-urlencoded and text/* - are all configured to allow compression. Compression occurs as the last step of conversion and only if the HTTP client sends the Accept-Encoding: gzip header.

Content types that are not in the codec repository will not trigger compression, even if the HTTP client allows compression with the Accept-Encoding header. This is to prevent binary contents like images from being 'compressed', since they are likely already compressed by a content-specific algorithm. In order for Aqueduct to compress a content type other than the built-in types, you may add a codec to the repository with the allowCompression flag. (The default value is true.)

CodecRegistry.add(
  ContentType("application", "x-special"),
   MyCodec(),
  allowCompression: true);

You may also set whether or not a content type uses compression without having to specify a codec if no conversion step needs to occur:

CodecRegistry.setAllowsCompression(new ContentType("application", "x-special"), true);

可序列化对象

Most request and response bodies are JSON objects and lists of objects. In Dart, JSON objects are maps. A Serializable object can be read from a map and converted back into a map. You subclass Serializable to assign keys from a map to properties of a your subclass, and to write its properties back to a map. This allows static types you declare in your application to represent expected request and response bodies. Aqueduct's ORM type ManagedObject is a Serializable, for example.

发送可序列化的对象作为响应体

The body object of a response can be a Serializable. Before the response is sent, asMap() is called before the body object is encoded into JSON (or some other transmission format).

For example, a single serializable object returned in a 200 OK response:

final query = Query<Person>(context)..where((p) => p.id).equalTo(1);
final person = await query.fetchOne();
final response = Response.ok(person);

A response body object can also be a list of Serializable objects.

final query = Query<Person>(context);
final people = await query.fetch();
final response = Response.ok(people);

The flow of a body object is shown in the following diagram. Each orange item is an allowed body object type and shows the steps it will go through when being encoded to the HTTP response body. For example, a Serializable goes through three steps, whereas a List<int> goes through zero steps and is added as-is to the HTTP response.

Response Body Object Flow

从请求体中读取可序列化对象

A serializable object can be read from a request body:

final person = Person()..read(await request.body.decode());

A list of serializable objects as well:

List<Map<String, dynamic>> objects = await request.body.decode();
final people = objects.map((o) => Person()..read(o)).toList();

Both serializable and a list of serializable can be bound to a operation method parameter in a ResourceController.

@Operation.post()
Future<Response> addPerson(@Bind.body() Person person) async {
  final insertedPerson = await context.insertObject(person);
  return Response.ok(insertedPerson);
}

Key 过滤

Both read and Bind.body (when binding a Serializable) support key filtering. A key filter is a list of keys that either discard keys from the body, requires keys in the body, or throws an error if a key exists in the body. Example:

final person = Person()
  ..read(await request.body.decode(),
         ignore: ["id"],
         reject: ["password"],
         require: ["name", "height", "weight"]);

In the above: if the body contains 'id', the value is discarded immediately; if the body contains 'password', a 400 status code exception is thrown; and if the body doesn't contain all of name, height and weight, a 400 status code exception is thrown.

When binding a list of serializables, filters are applied to each element of the list.

@Operation.post()
Future<Response> addPerson(@Bind.body(reject: ["privateInfo"]) List<Person> people) async {
  // if any Person in the body contains the privateInfo key, a 400 Bad Request is sent and this method
  // is not called
}

可序列化的子类

A Serializable object must implement a readFromMap() and asMap().

An object that extends Serializable may be used as a response body object directly:

class Person extends Serializable {
  String name;
  String email;

  Map<String, dynamic> asMap() {
    return {
      "name": name,
      "email": email
    };
  }

  void readFromMap(Map<String, dynamic> inputMap) {
    name = inputMap['name'];
    email = inputMap['email'];
  }
}

final person = Person();
final response = Response.ok(person);

readFromMap is invoked by read, after all filters have been applied.

可序列化和OpenAPI生成

请参阅Serializable类型如何与OpenAPI文档生成配合使用的部分 这里