mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-04-23 11:07:53 +02:00
HTTP components
This commit is contained in:
parent
019134b2e8
commit
204dc4e259
@ -21,7 +21,7 @@ Consider the following:
|
|||||||
* If there is a uniform communication interface, it can be mimicked if needed, so the requirement of client and server implementation independence can always be met.
|
* If there is a uniform communication interface, it can be mimicked if needed, so the requirement of client and server implementation independence can always be met.
|
||||||
* If we can create an alternative server, it means we can always have a layered architecture by placing an additional proxy between the client and the server.
|
* If we can create an alternative server, it means we can always have a layered architecture by placing an additional proxy between the client and the server.
|
||||||
* As clients are computational machines, they *always* store some state and cache some data.
|
* As clients are computational machines, they *always* store some state and cache some data.
|
||||||
* Finally, the code-on-demand requirement is a cunning one (at least in von Neumann architectures) as we can always say that the data the client receives actually comprises instructions in some formal language.
|
* Finally, the code-on-demand requirement is a sly one as in a [von Neumann architecture](https://en.wikipedia.org/wiki/Von_Neumann_architecture), we can always say that the data the client receives actually comprises instructions in some formal language.
|
||||||
|
|
||||||
Yes, of course, the reasoning above is a sophism, a reduction to absurdity. Ironically, we might take the opposite path to absurdity by proclaiming that REST constraints are never met. For instance, the code-on-demand requirement obviously contradicts the requirement of having an independently-implemented client and server as the client must be able to interpret the instructions the server sends written in a specific language. As for the “S” rule (i.e., the “stateless” constraint), it is very hard to find a system that does not store *any* client context as it's close to impossible to make anything *useful* for the client in this case. (And, by the way, Fielding explicitly requires that: “communication … cannot take advantage of any stored context on the server.”)
|
Yes, of course, the reasoning above is a sophism, a reduction to absurdity. Ironically, we might take the opposite path to absurdity by proclaiming that REST constraints are never met. For instance, the code-on-demand requirement obviously contradicts the requirement of having an independently-implemented client and server as the client must be able to interpret the instructions the server sends written in a specific language. As for the “S” rule (i.e., the “stateless” constraint), it is very hard to find a system that does not store *any* client context as it's close to impossible to make anything *useful* for the client in this case. (And, by the way, Fielding explicitly requires that: “communication … cannot take advantage of any stored context on the server.”)
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
### [Components of an HTTP Request and Their Semantics][http-api-requests-semantics]
|
### [Components of an HTTP Request and Their Semantics][http-api-requests-semantics]
|
||||||
|
|
||||||
Third import exercise we must conduct is to describe the format of HTTP request and response and explain basic concepts. Many of these might look obvious to the reader. However, the situation is that even the basic knowledge we require to move further is scattered across vast and fragmented documentation, making even experienced developers struggle with some nuances. Below, we will try to compile some structured overview sufficient to design HTTP APIs.
|
The third important exercise we must conduct is to describe the format of an HTTP request and response and explain the basic concepts. Many of these may seem obvious to the reader. However, the situation is that even the basic knowledge we require to move further is scattered across vast and fragmented documentation, causing even experienced developers to struggle with some nuances. Below, we will try to compile a structured overview that is sufficient to design HTTP APIs.
|
||||||
|
|
||||||
To describe semantics and formats, we will refer to brand new [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html) that replaced non less than nine previous specifications dealing with different aspects of the technology. Still, a significant volume of additional functionality is nevertheless covered by separate standards. In particular, the HTTP caching principles are described in standalone [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html), whereas the popular `PATCH` method is omitted in the main RFC and is regulated by [RFC 5789](https://www.rfc-editor.org/rfc/rfc5789.html).
|
To describe the semantics and formats, we will refer to the brand-new [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html), which replaces no fewer than nine previous specifications dealing with different aspects of the technology. However, a significant volume of additional functionality is still covered by separate standards. In particular, the HTTP caching principles are described in the standalone [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html), while the popular `PATCH` method is omitted in the main RFC and is regulated by [RFC 5789](https://www.rfc-editor.org/rfc/rfc5789.html).
|
||||||
|
|
||||||
An HTTP request stands for (1) applying a specific verb to a URL, stating (2) the protocol version, (3) additional meta-information in headers, and (4) optionally, some content (request body):
|
An HTTP request consists of (1) applying a specific verb to a URL, stating (2) the protocol version, (3) additional meta-information in headers, and (4) optionally, some content (request body):
|
||||||
|
|
||||||
```
|
```
|
||||||
POST /v1/orders HTTP/1.1
|
POST /v1/orders HTTP/1.1
|
||||||
@ -21,7 +21,7 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
An HTTP response to such a request will comprise (1) protocol version, (2) status code with a corresponding message, (3) response headers, and (4) optionally, response content (body):
|
An HTTP response to such a request includes (1) the protocol version, (2) a status code with a corresponding message, (3) response headers, and (4) optionally, response content (body):
|
||||||
|
|
||||||
```
|
```
|
||||||
HTTP/1.1 201 Created
|
HTTP/1.1 201 Created
|
||||||
@ -33,6 +33,136 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**NB**: in HTTP/2 (and future HTTP/3) instead of holistic text format, separate binary frames are used for headers and data. This doesn't affect the architectural concepts we will describe below. However, to avoid ambiguity we will give all examples in the HTTP/1.1 format. You may read about the HTTP/2 format in detail [here](https://hpbn.co/http2/).
|
**NB**: in HTTP/2 (and future HTTP/3), separate binary frames are used for headers and data instead of the holistic text format. However, this doesn't affect the architectural concepts we will describe below. To avoid ambiguity, we will provide examples in the HTTP/1.1 format. You can find detailed information about the HTTP/2 format [here](https://hpbn.co/http2/).
|
||||||
|
|
||||||
##### URL
|
##### A URL
|
||||||
|
|
||||||
|
A Uniform Resource Locator (URL) is an addressing unit in an HTTP API. Some evangelists of the technology even use the term “URL space” as a synonym for “The World Wide Web.” It is expected that a proper HTTP API should employ an addressing system that is as granular as the subject area itself; in other words, each entity that the API can manipulate should have its own URL.
|
||||||
|
|
||||||
|
The URL format is governed by a [separate standard](https://url.spec.whatwg.org/) developed by an independent body known as the Web Hypertext Application Technology Working Group (WHATWG). The concepts of URLs and Uniform Resource Names (URNs) together constitute a more general entity called Uniform Resource Identifiers (URIs). (The difference between the two is that a URL allows for *locating* a resource within the framework of some protocol whereas a URN is an “internal” entity name that does not provide information on how to find the resource.)
|
||||||
|
|
||||||
|
URLs are decomposed into sub-components, each of which is optional:
|
||||||
|
* A scheme: a protocol to access the resource (in our case it is always `https:`)
|
||||||
|
* A host: a top-level address unit in the form of either a domain name or an IP address. A host might contain subdomains.
|
||||||
|
* A port.
|
||||||
|
* A path: a URL part between the host (including port) and the `?` or `#` symbols or the end of the line.
|
||||||
|
* In practice, the path itself is usually decomposed into parts using the `/` symbol as a delimiter. However, the standard does not prescribe such decomposition or define any semantics for it.
|
||||||
|
* Two paths, one ending with `/` and one without it (for example, `/root/leaf` and `/root/leaf/`), are considered different paths according to the standard. Conversely, two URLs that differ only in trailing slashes in their paths are considered different as well. However, we are unaware of a single argument to differentiate such URLs in practice.
|
||||||
|
* Paths may contain `.` and `..` parts, which are supposed to be interpreted similarly to analogous symbols in file paths (meaning that `/root/leaf`, `/root/./leaf`, and `/root/branch/../leaf` are equivalent). However, the standard again does not prescribe this.
|
||||||
|
* A query: a URL part between the `?` symbol and either `#` or the end of the line.
|
||||||
|
* A query is usually decomposed into `key=value` pairs split by the `&` character. Again, the standard does not require this.
|
||||||
|
* Nor does the standard imply any normalization of the ordering. URLs that differ only in the order of keys in the queries are considered different.
|
||||||
|
* A fragment (also known as an anchor): a part of a URL that follows the `#` sign.
|
||||||
|
* Fragments are usually treated as addresses within the requested document and because of that are often omitted by user agents while executing the request.
|
||||||
|
* Two URLs that only differ in fragment parts may be considered equal or not, depending on the context.
|
||||||
|
|
||||||
|
In HTTP requests, the scheme, host, and port are usually (but not always) omitted and presumed to be equal to the connection parameters. (Fielding actually names this arrangement one of the biggest flaws in the protocol design.)
|
||||||
|
|
||||||
|
**NB**: the standard also enumerates some legacy components such as logins and passwords in URLs or non-UTF encoding marks, which we consider irrelevant to the topic of API design. Additionally, the standard contains rules for serializing, normalizing, and comparing URLs, knowing which can be useful for an HTTP API developer.
|
||||||
|
|
||||||
|
##### Headers
|
||||||
|
|
||||||
|
Headers contain *metadata* associated with a request or a response. They might describe properties of entities being passed (e.g., `Content-Length`), provide additional information regarding a client or a server (e.g., `User-Agent`, `Date`, etc.) or simply contain additional fields that are not directly related to the request/response semantics (such as `Authorization`).
|
||||||
|
|
||||||
|
The important feature of headers is the possibility to read them before the message body is fully transmitted. This allows for altering request or response handling depending on the headers, and it is perfectly fine to manipulate headers while proxying requests. Many network agents actually do this, i.e., add, remove, or modify headers while proxying requests. In particular, modern web browsers automatically add a number of technical headers, such as `User-Agent`, `Origin`, `Accept-Language`, `Connection`, `Referer`, `Sec-Fetch-*`, etc., and modern server software automatically adds or modifies such headers as `X-Powered-By`, `Date`, `Content-Length`, `Content-Encoding`, `X-Forwarded-For`, etc.
|
||||||
|
|
||||||
|
This freedom in manipulating headers can result in unexpected problems if an API uses them to transmit data as the field names developed by an API vendor can accidentally overlap with existing conventional headers, or worse, such a collision might occur in the future at any moment. To avoid this issue, the practice of adding the prefix `X-` to custom header names was frequently used in the past. More than ten years ago this practice was officially discouraged (see the detailed overview in [RFC 6648](https://www.rfc-editor.org/rfc/rfc6648)). Nevertheless, the prefix has not been fully abolished, and many semi-standard headers still contain it (notably, `X-Forwarded-For`). Therefore, using the `X-` prefix reduces the probability of collision but does not eliminate it. The same RFC reasonably suggests using the API vendor name as a prefix instead of `X-`. (We would rather recommend using both, i.e., sticking to the `X-ApiName-FieldName` format. Here `X-` is included for readability [to distinguish standard fields from custom ones], and the company or API name part helps avoid collisions with other non-standard header names).
|
||||||
|
|
||||||
|
Additionally, headers are used as control flow instructions for so-called “content negotiation,” which allows the client and server to agree on a response format (through `Accept*` headers) and to perform conditional requests that aim to reduce traffic by skipping response bodies, either fully or partially (through `If-*` headers, such as `If-Range`, `If-Modified-Since`, etc.)
|
||||||
|
|
||||||
|
##### HTTP Verbs
|
||||||
|
|
||||||
|
One important component of an HTTP request is a method (verb) that describes the operation being applied to a resource. RFC 9110 standardizes eight verbs — namely, `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `CONNECT`, `OPTIONS`, and `TRACE` — of which we as API developers are interested in the former four. The `CONNECT`, `OPTIONS`, and `TRACE` methods are technical and rarely used in HTTP APIs (except for `OPTIONS`, which needs to be implemented to ensure access to the API from a web browser). Theoretically, the `HEAD` verb, which allows for requesting *resource metadata only*, might be quite useful in API design. However, for reasons unknown to us, it did not take root in this capacity.
|
||||||
|
|
||||||
|
Apart from RFC 9110, many other specifications propose additional HTTP verbs, such as `COPY`, `LOCK`, `SEARCH`, etc. — the full list can be found in [the registry](http://www.iana.org/assignments/http-methods/http-methods.xhtml). However, only one of them gained widespread popularity — the `PATCH` method. The reasons for this state of affairs are quite trivial: the five methods (`GET`, `POST`, `PUT`, `DELETE`, and `PATCH`) are enough for almost any API.
|
||||||
|
|
||||||
|
HTTP verbs define two important characteristics of an HTTP call:
|
||||||
|
* Semantics: what the operation *means*
|
||||||
|
* Side effects:
|
||||||
|
* Whether the request modifies any resource state or if it is safe (and therefore, could it be cached)
|
||||||
|
* Whether the request is idempotent or not.
|
||||||
|
|
||||||
|
| Verb | Semantics | Is safe (non-modifying) | Is idempotent | Can have a body |
|
||||||
|
|------|-----------|-------------------------|---------------|------------|
|
||||||
|
| GET | Returns a representation of a resource | Yes | Yes | No |
|
||||||
|
| PUT | Replaces (fully overwrites) a resource with a provided entity | No | Yes | Yes |
|
||||||
|
| DELETE | Deletes a resource | No | Yes | No |
|
||||||
|
| POST | Processes a provided entity according to its internal semantics | No | No | Yes |
|
||||||
|
| PATCH | Modifies (partially overwrites) a resource with a provided entity | No | No | Yes |
|
||||||
|
|
||||||
|
The most important property of modifying idempotent verbs is that **the URL serves as an idempotency key for the request**. The `PUT /url` operation fully overwrites a resource, so repeating the request won't change the resource. Conversely, retrying a `DELETE /url` request must leave the system in the same state where the `/url` resource is deleted. Regarding the `GET /url` method, it must semantically return the representation of the same target resource `/url`. If it exists, its implementation must be consistent with prior `PUT` / `DELETE` operations. If the resource was overwritten via `PUT /url`, a subsequent `GET /url` call must return a representation that matches the entity enclosed in the `PUT /url` request. In the case of JSON-over-HTTP APIs, this simply means that `GET /url` returns the same data as what was passed in the preceding `PUT /url`, possibly normalized and equipped with default values. On the other hand, a `DELETE /url` call must remove the resource, resulting in subsequent `GET /url` requests returning a `404` or `410` error.
|
||||||
|
|
||||||
|
The idempotency and symmetry of the `GET` / `PUT` / `DELETE` methods imply that neither `GET` nor `DELETE` can have a body as no reasonable meaning could be associated with it. However, most web server software allows these methods to have bodies and transmits them further to the endpoint handler, likely because many software engineers are unaware of the semantics of the verbs (although we strongly discourage relying on this behavior).
|
||||||
|
|
||||||
|
For obvious reasons, responses to modifying endpoints are not cached (though there are some conditions to use a response to a `POST` request as cached data for subsequent `GET` requests). This ensures that repeating `POST` / `PUT` / `DELETE` / `PATCH` requests will hit the server as no intermediary agent can respond with a cached result. In the case of a `GET` request, it is generally not true. Only the presence of `no-store` or `no-cache` directives in the response guarantees that the subsequent `GET` request will reach the server.
|
||||||
|
|
||||||
|
One of the most widespread HTTP API design antipatterns is violating the semantics of HTTP verbs:
|
||||||
|
* Placing modifying operations in a `GET` handler. This can lead to the following problems:
|
||||||
|
* Interim agents might respond to such a request using a cached value if a required caching directive is missing, or vice versa, automatically repeat a request upon receiving a network timeout.
|
||||||
|
* Some agents consider themselves eligible to traverse hyper-references (i.e., making HTTP `GET` requests) without the explicit user's consent. For example, social networks and messengers perform such calls to generate a preview for a link when a user tries to share it.
|
||||||
|
* Placing non-idempotent operations in `PUT` / `DELETE` handlers. Although interim agents do not typically repeat modifying requests regardless of their alleged idempotency, a client or server framework can easily do so. This mistake is often coupled with requiring passing a body alongside a `DELETE` request to discern the specific object that needs to be deleted, which per se is a problem as any interim agent might discard such a body.
|
||||||
|
* Ignoring the `GET` / `PUT` / `DELETE` symmetry requirement. This can manifest in different ways, such as:
|
||||||
|
* Making a `GET /url` operation return data even after a successful `DELETE /url` call
|
||||||
|
* Making a `PUT /url` operation take the identifiers of the entities to modify from the request body instead of the URL, resulting in the `GET /url` operation's inability to return a representation of the entity passed to the `PUT /url` handler.
|
||||||
|
|
||||||
|
##### Status Codes
|
||||||
|
|
||||||
|
A status code is a machine-readable three-digit number that describes the outcome of an HTTP request. There are five groups of status codes:
|
||||||
|
* `1xx` codes are informational. Among these, the `100 Continue` code is probably the only one that is commonly used.
|
||||||
|
* `2xx` codes indicate that the operation was successful.
|
||||||
|
* `3xx` codes are redirection codes, implying that additional actions must be taken to consider the operation fully successful.
|
||||||
|
* `4xx` codes represent client errors
|
||||||
|
* `5xx` codes represent server errors.
|
||||||
|
|
||||||
|
**NB**: the separation of codes into groups by the first digit is of practical importance. If the client is unaware of the meaning of an `xyz` code returned by the server, it must conduct actions as if an `x00` code was received.
|
||||||
|
|
||||||
|
The idea behind status codes is obviously to make errors machine-readable so that all interim agents can detect what has happened with a request. The HTTP status code nomenclature effectively describes nearly every problem applicable to an HTTP request, such as invalid `Accept-*` header values, missing `Content-Length`, unsupported HTTP verbs, excessively long URIs, etc.
|
||||||
|
|
||||||
|
Unfortunately, the HTTP status code nomenclature is not well-suited for describing errors in *business logic*. To return machine-readable errors related to the semantics of the operation, it is necessary either to use status codes unconventionally (i.e., in violation of the standard) or to enrich responses with additional fields. Designing custom errors in HTTP APIs will be discussed in the corresponding chapter.
|
||||||
|
|
||||||
|
**NB**: note the problem with the specification design. By default, all `4xx` codes are non-cacheable, but there are several exceptions, namely the `404`, `405`, `410`, and `414` codes. While we believe this was done with good intentions, the number of developers aware of this nuance is likely to be similar to the number of HTTP specification editors.
|
||||||
|
|
||||||
|
#### One Important Remark Regarding Caching
|
||||||
|
|
||||||
|
Caching is a crucial aspect of modern microservice architecture design. It can be tempting to control caching at the protocol level, and the HTTP standard provides various tools to facilitate this. However, the author of this book must warn you: if you decide to utilize these tools, it is essential to thoroughly understand the standard. Flaws in the implementation of certain techniques can result in disruptive behavior. The author personally experienced a major outage caused by the aforementioned lack of knowledge regarding the default cacheability of `404` responses. In this incident, some settings for an important geographical area were mistakenly deleted. Although the problem was quickly localized and the settings were restored, the service remained inoperable in the area for several hours because clients had cached the `404` response and did not request it anew until the cache had expired.
|
||||||
|
|
||||||
|
#### One Important Remark Regarding Consistency
|
||||||
|
|
||||||
|
One parameter might be placed in different components of an HTTP request. For example, an identifier of a partner making a request might be passed as part of:
|
||||||
|
* A domain name, e.g., `{partner_id}.domain.tld`
|
||||||
|
* A path, e.g., `/v1/{partner_id}/orders`
|
||||||
|
* A query parameter, e.g. `/v1/orders?partner_id=<partner_id>`
|
||||||
|
* A header value, e.g.
|
||||||
|
```
|
||||||
|
GET /v1/orders HTTP/1.1
|
||||||
|
X-ApiName-Partner-Id: <partner_id>
|
||||||
|
```
|
||||||
|
* A field within the request body, e.g.
|
||||||
|
```
|
||||||
|
POST /v1/orders/retrieve HTTP/1.1
|
||||||
|
|
||||||
|
{
|
||||||
|
"partner_id": <partner_id>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There are also more exotic options, such as placing a parameter in the scheme of a request or in the `Content-Type` header.
|
||||||
|
|
||||||
|
However, when we move a parameter around different components, we face three annoying issues:
|
||||||
|
* Some tokens are case-sensitive (path, query parameters, JSON field names), while others are not (domain and header names)
|
||||||
|
* With header *values*, there is even more chaos: some of them are required to be case-insensitive (e.g., `Content-Type`), while others are prescribed to be case-sensitive (e.g., `ETag`)
|
||||||
|
* Allowed symbols and escaping rules differ as well:
|
||||||
|
* Notably, there is no widespread practice for escaping the `/`, `?`, and `#` symbols in a path
|
||||||
|
* Unicode symbols in domain names are allowed (though not universally supported) through a peculiar encoding technique called “[Punycode](https://www.rfc-editor.org/rfc/rfc3492.txt)”
|
||||||
|
* Traditionally, different casings are used in different parts of an HTTP request:
|
||||||
|
* `kebab-case`in domains, headers, and paths
|
||||||
|
* `snake_case` in query parameters
|
||||||
|
* `snake_case` or `camelCase` in request bodies.
|
||||||
|
|
||||||
|
Furthermore, using both `snake_case` and `camelCase` in domain names is impossible as the underscore sign is not allowed and capital letters will be lowercased during URL normalization.
|
||||||
|
|
||||||
|
Theoretically, it is possible to use `kebab-case` everywhere. However, most programming languages do not allow variable names and object fields in `kebab-case`, so working with such an API would be quite inconvenient.
|
||||||
|
|
||||||
|
To wrap this up, the situation with casing is so spoiled and convoluted that there is no consistent solution to employ. In this book, we follow this rule: tokens are cased according to the common practice for the corresponding request component. If a token's position changes, the casing is changed as well. (However, we're far from recommending following this approach unconditionally. Our recommendation is rather to try to avoid increasing the entropy by choosing a solution that minimizes the probability of misunderstanding.)
|
||||||
|
|
||||||
|
**NB**: strictly speaking, JSON stands for “JavaScript Object Notation,” and in JavaScript, the default casing is `camelCase`. However, we dare to say that JSON ceased to be a format bound to JavaScript long ago and is now a universal format for organizing communication between agents written in different programming languages. Employing `camel_case` allows for easily moving a parameter from a query to a body, which is the most frequent case. Although the inverse solution (i.e., using `camelCase` in query parameter names) is also possible.
|
@ -22,7 +22,7 @@
|
|||||||
* раз есть интерфейс взаимодействия, значит, под него всегда можно мимикрировать, а значит, требование независимости имплементации клиента и сервера всегда выполнимо;
|
* раз есть интерфейс взаимодействия, значит, под него всегда можно мимикрировать, а значит, требование независимости имплементации клиента и сервера всегда выполнимо;
|
||||||
* раз можно сделать альтернативную имплементацию сервера — значит, можно сделать и многослойную архитектуру, поставив дополнительный прокси между клиентом и сервером;
|
* раз можно сделать альтернативную имплементацию сервера — значит, можно сделать и многослойную архитектуру, поставив дополнительный прокси между клиентом и сервером;
|
||||||
* поскольку клиент представляет собой вычислительную машину, он всегда хранит хоть какое-то состояние и кэширует хоть какие-то данные;
|
* поскольку клиент представляет собой вычислительную машину, он всегда хранит хоть какое-то состояние и кэширует хоть какие-то данные;
|
||||||
* наконец, code-on-demand вообще лукавое требование, поскольку всегда можно объявить данные, полученные по сети, «инструкциями» на некотором формальном языке, а код клиента — их интерпретатором.
|
* наконец, code-on-demand вообще лукавое требование, поскольку в [архитектуре фон Неймана](https://en.wikipedia.org/wiki/Von_Neumann_architecture) всегда можно объявить данные, полученные по сети, «инструкциями» на некотором формальном языке, а код клиента — их интерпретатором.
|
||||||
|
|
||||||
Да, конечно, вышеприведённое рассуждение является софизмом, доведением до абсурда. Самое забавное в этом упражнении состоит в том, что мы можем довести его до абсурда и в другую сторону, объявив ограничения REST неисполнимыми. Например, очевидно, что требование code-on-demand противоречит требованию независимости клиента и сервера — клиент должен уметь интерпретировать код с сервера, написанный на вполне конкретном языке. Что касается правила на букву S («stateless»), то систем, в которых сервер *вообще не хранит никакого контекста клиента* в мире вообще практически нет, поскольку почти ничего полезного для клиента в такой системе сделать нельзя. (Чего, кстати, Филдинг прямым текстом требует: «коммуникация … не может получать никаких преимуществ от того, что на сервере хранится какой-то контекст».)
|
Да, конечно, вышеприведённое рассуждение является софизмом, доведением до абсурда. Самое забавное в этом упражнении состоит в том, что мы можем довести его до абсурда и в другую сторону, объявив ограничения REST неисполнимыми. Например, очевидно, что требование code-on-demand противоречит требованию независимости клиента и сервера — клиент должен уметь интерпретировать код с сервера, написанный на вполне конкретном языке. Что касается правила на букву S («stateless»), то систем, в которых сервер *вообще не хранит никакого контекста клиента* в мире вообще практически нет, поскольку почти ничего полезного для клиента в такой системе сделать нельзя. (Чего, кстати, Филдинг прямым текстом требует: «коммуникация … не может получать никаких преимуществ от того, что на сервере хранится какой-то контекст».)
|
||||||
|
|
||||||
|
@ -37,28 +37,29 @@ Content-Type: application/json
|
|||||||
|
|
||||||
##### URL
|
##### URL
|
||||||
|
|
||||||
URL — единица адресации в HTTP API (некоторые евангелисты технологии даже используют термин «пространство URL» как синоним для Мировой паутины). Предполагается, что API в парадигме REST должно использовать систему адресов столь же гранулярную, как и предметная область; иными словами, у любых сущностей, которыми мы можем манипулировать независимо, должен быть свой URL.
|
URL — единица адресации в HTTP API (некоторые евангелисты технологии даже используют термин «пространство URL» как синоним для Мировой паутины). Предполагается, что HTTP API должен использовать систему адресов столь же гранулярную, как и предметная область; иными словами, у любых сущностей, которыми мы можем манипулировать независимо, должен быть свой URL.
|
||||||
|
|
||||||
Формат URL регулируется [отдельным стандартом](https://url.spec.whatwg.org/), который развивает независимое сообщество Web Hypertext Application Technology Working Group (WHATWG). Считается, что концепция URL (вместе с понятием универсального имени ресурса, URN) составляет более общую сущность URI (универсальный идентификатор ресурса). (Разница между URL и URN заключается в том, что URL позволяет *найти* некоторый ресурс в рамках некоторого протокола доступа, в то время как URN — «внутреннее» имя объекта, которое само по себе никак не помогает получить к нему доступ.)
|
Формат URL регулируется [отдельным стандартом](https://url.spec.whatwg.org/), который развивает независимое сообщество Web Hypertext Application Technology Working Group (WHATWG). Считается, что концепция URL (вместе с понятием универсального имени ресурса, URN) составляет более общую сущность URI (универсальный идентификатор ресурса). (Разница между URL и URN заключается в том, что URL позволяет *найти* некоторый ресурс в рамках некоторого протокола доступа, в то время как URN — «внутреннее» имя объекта, которое само по себе никак не помогает получить к нему доступ.)
|
||||||
|
|
||||||
URL принято раскладывать на составляющие, каждая из которых может отсутствовать:
|
URL принято раскладывать на составляющие, каждая из которых опциональна:
|
||||||
* схема (scheme) — протокол обращения (в нашем случае всегда `https:`);
|
* схема (scheme) — протокол обращения (в нашем случае всегда `https:`);
|
||||||
* хост (host; домен или IP-адрес), к которому обращён запрос — самая крупная единица адресации;
|
* хост (host; домен или IP-адрес) — самая крупная единица адресации;
|
||||||
* домен может включать в себя поддомены;
|
* домен может включать в себя поддомены;
|
||||||
* порт (port);
|
* порт (port);
|
||||||
* путь (path) — часть URL между доменным именем (с портом) и символами `?`, `#` или концом строки
|
* путь (path) — часть URL между именем хоста (с портом) и символами `?`, `#` или концом строки
|
||||||
* путь принято разбивать по символу `/` и работать с каждой частью как отдельным токеном — но стандарт этого, вообще говоря, не предписывает;
|
* путь принято разбивать по символу `/` и работать с каждой частью как отдельным токеном — но стандарт этого, вообще говоря, не предписывает;
|
||||||
* пути с символом `/` и без символа `/` в конце (скажем `/root/leaf` и `/root/leaf/`) с точки зрения стандарта являются разными (и URL, отличающиеся только наличием/отсутствием слэша, считаются разными URL), хотя практически нам неизвестны аргументы в пользу того, чтобы не считать такие пути эквивалентными;
|
* пути с символом `/` и без символа `/` в конце (скажем `/root/leaf` и `/root/leaf/`) с точки зрения стандарта являются разными (и URL, отличающиеся только наличием/отсутствием слэша, считаются разными URL), хотя практически нам неизвестны аргументы в пользу того, чтобы не считать такие пути эквивалентными;
|
||||||
* пути могут содержать секции `.` и/или `..`, которые предлагается трактовать по аналогии с такими же символами в путях на файловой системе (и, соответственно, считать URL `/root/leaf`, `/root/./leaf`, `/root/branch/../leaf` эквивалентными), но стандарт этого вновь не предписывает;
|
* пути могут содержать секции `.` и/или `..`, которые предлагается трактовать по аналогии с такими же символами в путях на файловой системе (и, соответственно, считать URL `/root/leaf`, `/root/./leaf`, `/root/branch/../leaf` эквивалентными), но стандарт этого вновь не предписывает;
|
||||||
* запрос (query) — часть URL после знака `?` до знака `#` или конца строки;
|
* запрос (query) — часть URL после знака `?` до знака `#` или конца строки;
|
||||||
* query принято раскладывать на пары `ключ=значение`, разделённые символом `&`; следует вновь иметь в виду, что стандарт не предписывает query строго соответствовать этому формату;
|
* query принято раскладывать на пары `ключ=значение`, разделённые символом `&`; следует вновь иметь в виду, что стандарт не предписывает query строго соответствовать этому формату;
|
||||||
|
* также стандарт не предписывает никакой нормализации — два URL, которые различаются только порядком ключей в query, по стандарту являются разными URL;
|
||||||
* фрагмент (fragment; также якорь, anchor) — часть URL после знака `#`;
|
* фрагмент (fragment; также якорь, anchor) — часть URL после знака `#`;
|
||||||
* фрагмент традиционно рассматривается как адресация внутри запрошенного документа, поэтому многими агентами опускается при выполнении запроса;
|
* фрагмент традиционно рассматривается как адресация внутри запрошенного документа, поэтому многими агентами опускается при выполнении запроса;
|
||||||
* два URL с различными значениями фрагмента могут считаться одинаковыми — а могут не считаться, зависит от контекста.
|
* два URL, отличающихся только значением фрагмента, могут считаться одинаковыми — а могут не считаться, зависит от контекста.
|
||||||
|
|
||||||
В HTTP-запросах, как правило (но не обязательно) схема, хост и порт опускаются (и считаются совпадающими с параметрами соединения). (Это соглашение, кстати, Филдинг считает самой большой проблемой дизайна протокола.)
|
В HTTP-запросах, как правило (но не обязательно) схема, хост и порт опускаются (и считаются совпадающими с параметрами соединения). (Это соглашение, кстати, Филдинг считает самой большой проблемой дизайна протокола.)
|
||||||
|
|
||||||
**NB**: в стандарте также перечислены разнообразные исторические наслоения (например, передача логинов и паролей в URL или использование не-UTF кодировки), которые нам в рамках вопросов дизайна API неинтересны. Также стандарт содержит правила сериализации, нормализации и сравнения URL, которые в целом полезно знать разработчику HTTP API.
|
**NB**: помимо указанных компонентов в стандарте перечислены разнообразные исторические наслоения (например, передача логинов и паролей в URL или использование не-UTF кодировки), которые нам в рамках вопросов дизайна API неинтересны. Также стандарт содержит правила сериализации, нормализации и сравнения URL, которые в целом полезно знать разработчику HTTP API.
|
||||||
|
|
||||||
##### Заголовки
|
##### Заголовки
|
||||||
|
|
||||||
@ -66,13 +67,13 @@ URL принято раскладывать на составляющие, ка
|
|||||||
|
|
||||||
Важное свойство заголовков — это возможность считывать их до того, как получено тело сообщения. Таким образом, заголовки могут, во-первых, сами по себе влиять на обработку запроса или ответа, и ими можно относительно легко манипулировать при проксировании — и многие сетевые агенты действительно это делают, добавляя или модифицируя заголовки по своему усмотрению (в частности, современные веб-браузеры добавляют к запросам целую коллекцию заголовков: `User-Agent`, `Origin`, `Accept-Language`, `Connection`, `Referer`, `Sec-Fetch-*` и так далее, а современное ПО веб-серверов, в свою очередь, автоматически добавляет или модифицирует такие заголовки как `X-Powered-By`, `Date`, `Content-Length`, `Content-Encoding`, `X-Forwarded-For`).
|
Важное свойство заголовков — это возможность считывать их до того, как получено тело сообщения. Таким образом, заголовки могут, во-первых, сами по себе влиять на обработку запроса или ответа, и ими можно относительно легко манипулировать при проксировании — и многие сетевые агенты действительно это делают, добавляя или модифицируя заголовки по своему усмотрению (в частности, современные веб-браузеры добавляют к запросам целую коллекцию заголовков: `User-Agent`, `Origin`, `Accept-Language`, `Connection`, `Referer`, `Sec-Fetch-*` и так далее, а современное ПО веб-серверов, в свою очередь, автоматически добавляет или модифицирует такие заголовки как `X-Powered-By`, `Date`, `Content-Length`, `Content-Encoding`, `X-Forwarded-For`).
|
||||||
|
|
||||||
Подобное вольное обращение с заголовками создаёт определённые проблемы, если ваш API предусматривает передачу дополнительных полей метаданных, поскольку придуманные вами имена полей могут случайно совпасть с какими-то из существующих стандартных имён (или ещё хуже — в будущем появится новое стандартное поле, совпадающее с вашим). Долгое время во избежание подобных коллизий использовался префикс `X-`; уже более 10 лет как эта практика объявлена устаревшей и не рекомендуется к использованию (см. подробный разбор вопроса в [RFC 6648](https://www.rfc-editor.org/rfc/rfc6648)), однако отказа от этого префикса по факту не произошло (и многие широко распространённые нестандартные заголовки, например, `X-Forwarded-For`, его всё ещё содержат). Таким образом, использование префикса `X-` вероятность коллизий снижает, но не устраняет. Тот же RFC вполне разумно предлагает использовать вместо `X-` префикс в виде имени компании. (Мы со своей стороны склонны рекомендовать использовать оба префикса в формате `X-ApiName-Field`; префикс `X-` для читабельности [чтобы отличать специальные заголовки от стандартных], а префикс с именем компании или API — чтобы не произошло коллизий с каким-нибудь другим нестандартным префиксом.)
|
Подобное вольное обращение с заголовками создаёт определённые проблемы, если ваш API предусматривает передачу дополнительных полей метаданных, поскольку придуманные вами имена полей могут случайно совпасть с какими-то из существующих стандартных имён (или ещё хуже — в будущем появится новое стандартное поле, совпадающее с вашим). Долгое время во избежание подобных коллизий использовался префикс `X-`; уже более 10 лет как эта практика объявлена устаревшей и не рекомендуется к использованию (см. подробный разбор вопроса в [RFC 6648](https://www.rfc-editor.org/rfc/rfc6648)), однако отказа от этого префикса по факту не произошло (и многие широко распространённые нестандартные заголовки, например, `X-Forwarded-For`, его всё ещё содержат). Таким образом, использование префикса `X-` вероятность коллизий снижает, но не устраняет. Тот же RFC вполне разумно предлагает использовать вместо `X-` префикс в виде имени компании. (Мы со своей стороны склонны рекомендовать использовать оба префикса в формате `X-ApiName-FieldName`; префикс `X-` для читабельности [чтобы отличать специальные заголовки от стандартных], а префикс с именем компании или API — чтобы не произошло коллизий с каким-нибудь другим нестандартным префиксом.)
|
||||||
|
|
||||||
Помимо прочего заголовки используются как управляющие конструкции — это т.н. «content negotiation», т.е. договорённость клиента и сервера о формате ответа (через заголовки `Accept*`) и условные запросы, позволяющие сэкономить трафик на возврате ответа целиком или частично (через заголовки `If-*`-заголовки, такие как `If-Range`, `If-Modified-Since` и так далее).
|
Помимо прочего заголовки используются как управляющие конструкции — это т.н. «content negotiation», т.е. договорённость клиента и сервера о формате ответа (через заголовки `Accept*`) и условные запросы, позволяющие сэкономить трафик на возврате ответа целиком или частично (через заголовки `If-*`-заголовки, такие как `If-Range`, `If-Modified-Since` и так далее).
|
||||||
|
|
||||||
##### HTTP-глаголы
|
##### HTTP-глаголы
|
||||||
|
|
||||||
Важнейшая составляющая HTTP запроса — это глагол (метод), описывающий операцию, применяемую к ресурсу. RFC 9110 описывает восемь глаголов (`GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `CONNECT`, `OPTIONS` и `TRACE`), из которых нас как разработчиков API интересует первые четыре. `CONNECT`, `OPTIONS` и `TRACE` — технические методы, которые очень редко используются в HTTP API (за исключением `OPTIONS`, который необходимо реализовать, если необходим доступ к API из браузера). Теоретически, `HEAD` (метод получения *только метаданных*, то есть заголовков, ресурса) мог бы быть весьма полезен в HTTP API, но по неизвестным нам причинам практически в этом смысле не используется.
|
Важнейшая составляющая HTTP запроса — это глагол (метод), описывающий операцию, применяемую к ресурсу. RFC 9110 стандартизирует восемь глаголов — `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `CONNECT`, `OPTIONS` и `TRACE` — из которых нас как разработчиков API интересует первые четыре. `CONNECT`, `OPTIONS` и `TRACE` — технические методы, которые очень редко используются в HTTP API (за исключением `OPTIONS`, который необходимо реализовать, если необходим доступ к API из браузера). Теоретически, `HEAD` (метод получения *только метаданных*, то есть заголовков, ресурса) мог бы быть весьма полезен в HTTP API, но по неизвестным нам причинам практически в этом смысле не используется.
|
||||||
|
|
||||||
Помимо RFC 9110, множество других RFC предлагают использовать дополнительные HTTP-глаголы (такие, например, как `COPY`, `LOCK`, `SEARCH` — полный список можно найти [здесь](http://www.iana.org/assignments/http-methods/http-methods.xhtml)), однако из всего разнообразия предложенных стандартов лишь один имеет широкое хождение — метод `PATCH`. Причины такого положения дел довольно тривиальны — этих пяти методов (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) достаточно для почти любого HTTP API.
|
Помимо RFC 9110, множество других RFC предлагают использовать дополнительные HTTP-глаголы (такие, например, как `COPY`, `LOCK`, `SEARCH` — полный список можно найти [здесь](http://www.iana.org/assignments/http-methods/http-methods.xhtml)), однако из всего разнообразия предложенных стандартов лишь один имеет широкое хождение — метод `PATCH`. Причины такого положения дел довольно тривиальны — этих пяти методов (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) достаточно для почти любого HTTP API.
|
||||||
|
|
||||||
@ -82,32 +83,31 @@ HTTP-глагол определяет два важных свойства HTTP
|
|||||||
* является ли запрос модифицирующим (и можно ли кэшировать ответ);
|
* является ли запрос модифицирующим (и можно ли кэшировать ответ);
|
||||||
* является ли запрос идемпотентным.
|
* является ли запрос идемпотентным.
|
||||||
|
|
||||||
| Глагол | Семантика | Безопасный (немодифицирующий) | Идемпотентный | Имеет тело |
|
| Глагол | Семантика | Безопасный (немодифицирующий) | Идемпотентный | Может иметь тело |
|
||||||
|--------|----------------------------------|---------------|---------------|------------|
|
|--------|----------------------------------|---------------|---------------|------------|
|
||||||
| GET | Возвращает представление ресурса | да | да | нет |
|
| GET | Возвращает представление ресурса | да | да | нет |
|
||||||
| PUT | Заменяет (полностью перезаписывает) ресурс согласно данным, переданным в теле запроса | да | да | да |
|
| PUT | Заменяет (полностью перезаписывает) ресурс согласно данным, переданным в теле запроса | нет | да | да |
|
||||||
| DELETE | Удаляет ресурс | да | да | нет |
|
| DELETE | Удаляет ресурс | нет | да | нет |
|
||||||
| POST | Обрабатывает запрос в соответствии со своим внутренним устройством | да | нет | да |
|
| POST | Обрабатывает запрос в соответствии со своим внутренним устройством | нет | нет | да |
|
||||||
| PATCH | Модифицирует (частично перезаписывает) ресурс согласно данным, переданным в теле запроса | да | нет | да |
|
| PATCH | Модифицирует (частично перезаписывает) ресурс согласно данным, переданным в теле запроса | нет | нет | да |
|
||||||
|
|
||||||
Важное свойство модифицирующих идемпотентных глаголов — это то, что **URL запроса является его ключом идемпотентности**. `PUT /url` полностью перезаписывает ресурс так, что `GET /url` возвращает представление, соответствующее переданном в `PUT /url` телу (в случае JSON-over-HTTP API это, как правило, просто означает, что `GET /url` возвращает в точности тот же контент, чтобы передан в `PUT /url`, с точностью до значений полей по умолчанию). `DELETE /url` обязан удалить указанный ресурс — так, что `GET /url` должен вернуть `404` или `410`.
|
Важное свойство модифицирующих идемпотентных глаголов — это то, что **URL запроса является его ключом идемпотентности**. `PUT /url` полностью перезаписывает ресурс, заданный своим URL (`/url`), и, таким образом, повтор запроса не изменяет ресурс. Аналогично, повторный вызов `DELETE /url` должен оставить систему в том же состоянии (ресурс `/url` удалён). Учитывая, что метод `GET /url` семантически должен вернуть представление целевого ресурса `/url`, то, если этот метод реализован, он должен возвращать консистентное предыдущим `PUT` / `DELETE` представление. Если ресурс был перезаписан через `PUR /url`, `GET /url` должен вернуть представление, соответствующее переданном в `PUT /url` телу (в случае JSON-over-HTTP API это, как правило, просто означает, что `GET /url` возвращает в точности тот же контент, чтобы передан в `PUT /url`, с точностью до значений полей по умолчанию). `DELETE /url` обязан удалить указанный ресурс — так, что `GET /url` должен вернуть `404` или `410`.
|
||||||
|
|
||||||
Идемпотентность и симметричность методов `GET` / `PUT` / `DELETE` влечёт за собой запрет `GET` и `DELETE` иметь тело запроса (поскольку этому телу невозможно приписать никакой осмысленной роли). Однако (по-видимому в связи с тем, что многие разработчики попросту не знают семантику этих методов) распространённое ПО веб-серверов обычно разрешает этим методам иметь тело запроса и транслирует его дальше к коду обработки эндпойнта (использование этой практики мы решительно не рекомендуем).
|
Идемпотентность и симметричность методов `GET` / `PUT` / `DELETE` влечёт за собой запрет `GET` и `DELETE` иметь тело запроса (поскольку этому телу невозможно приписать никакой осмысленной роли). Однако (по-видимому в связи с тем, что многие разработчики попросту не знают семантику этих методов) распространённое ПО веб-серверов обычно разрешает этим методам иметь тело запроса и транслирует его дальше к коду обработки эндпойнта (использование этой практики мы решительно не рекомендуем).
|
||||||
|
|
||||||
Достаточно очевидным образом ответы на модифицирующие запросы не кэшируются (хотя при определённых условиях закэшированный ответ метода `POST` может быть использован при последующем `GET`-запросе) и, таким образом, повторный `POST` / `PUT` / `DELETE` / `PATCH` запрос обязательно будет доставлен до конечного сервера (ни один промежуточный агент не имеет права ответить из кэша). В случае `GET`-запроса это, вообще говоря, неверно — гарантией может служить только наличие в ответе директив кэширования `no-store` или `no-cache`.
|
Достаточно очевидным образом ответы на модифицирующие запросы не кэшируются (хотя при определённых условиях закэшированный ответ метода `POST` может быть использован при последующем `GET`-запросе) и, таким образом, повторный `POST` / `PUT` / `DELETE` / `PATCH` запрос обязательно будет доставлен до конечного сервера (ни один промежуточный агент не имеет права ответить из кэша). В случае `GET`-запроса это, вообще говоря, неверно — гарантией может служить только наличие в ответе директив кэширования `no-store` или `no-cache`.
|
||||||
|
|
||||||
Один из самых частых антипаттернов разработки HTTP API — это использование HTTP-глаголов в нарушение их семантики:
|
Один из самых частых антипаттернов разработки HTTP API — это использование HTTP-глаголов в нарушение их семантики:
|
||||||
* размещение модифицирующих операций за `GET`
|
* Размещение модифицирующих операций за `GET`:
|
||||||
* промежуточные агенты могут ответить на такой запрос из кэша, если какая-то из директив кэширования отсутствует, либо, напротив, повторить запрос при получении сетевого таймаута;
|
* промежуточные агенты могут ответить на такой запрос из кэша, если какая-то из директив кэширования отсутствует, либо, напротив, повторить запрос при получении сетевого таймаута;
|
||||||
* некоторые агенты считают себя вправе переходить по таким ссылкам без явного волеизъявления пользователя или разработчика; например, социальные сети и мессенджеры выполняют такие вызовы для генерации оформления ссылки, если пользователь пытается ей поделиться;
|
* некоторые агенты считают себя вправе переходить по таким ссылкам без явного волеизъявления пользователя или разработчика; например, социальные сети и мессенджеры выполняют такие вызовы для генерации оформления ссылки, если пользователь пытается ей поделиться.
|
||||||
* размещение неидемпотентных операций за идемпотентными методами `PUT` / `DELETE`
|
* Размещение неидемпотентных операций за идемпотентными методами `PUT` / `DELETE`. хотя промежуточные агенты редко автоматически повторяют модифицирующие запросы, тем не менее это легко может сделать используемый разработчиком клиента или сервера фреймворк. Обычно эта ошибка сочетается с наличием у `DELETE`-запроса тела (чтобы всё-таки отличать, что конкретно нужно перезаписать или удалить), что является само по себе проблемой, так как любой сетевой агент вправе это тело проигнорировать.
|
||||||
* хотя промежуточные агенты редко автоматически повторяют модифицирующие запросы, тем не менее это легко может сделать используемый разработчиком клиента или сервера фреймворк;
|
* Несоблюдение требования симметричности операций `GET` / `PUT` / `DELETE`:
|
||||||
* обычно эта ошибка сочетается с наличием у запроса тела (чтобы всё-таки отличать, что конкретно нужно перезаписать или удалить), что является само по себе проблемой, так как любой сетевой агент вправе это тело проигнорировать;
|
* например, после выполнения `DELETE /url` операция `GET /url` продолжает возвращать какие-то данные или `PUT /url` ориентируется не на URL, а на данные внутри тела запроса для определения сущности, над которой выполняется операция, и, таким образом, `GET /url` никак не может вернуть представление объекта, только что переданного в `PUT /url`.
|
||||||
* несоблюдение требования симметричности операций `GET` / `PUT` / `DELETE` (т.е., например, после выполнения `DELETE /url` операция `GET /url` продолжает возвращать какие-то данные).
|
|
||||||
|
|
||||||
#### Статус-коды
|
##### Статус-коды
|
||||||
|
|
||||||
Статус-код — это численное машиночитаемое описание результата операции. Все статус-коды делятся на пять больших групп:
|
Статус-код — это машиночитаемое описание результата HTTP-запроса в виде трёхзначного числа. Все статус-коды делятся на пять больших групп:
|
||||||
* `1xx` — информационные (фактически, какое-то хождение имеет разве что `100 Continue`);
|
* `1xx` — информационные (фактически, какое-то хождение имеет разве что `100 Continue`);
|
||||||
* `2xx` — коды успеха операции;
|
* `2xx` — коды успеха операции;
|
||||||
* `3xx` — коды перенаправлений (индицируют необходимость выполнения дополнительных действий, чтобы считать операцию успешной);
|
* `3xx` — коды перенаправлений (индицируют необходимость выполнения дополнительных действий, чтобы считать операцию успешной);
|
||||||
@ -118,10 +118,10 @@ HTTP-глагол определяет два важных свойства HTTP
|
|||||||
|
|
||||||
В основе технологии статус-кодов лежит понятное желание сделать ошибки машиночитаемыми, так, чтобы все промежуточные агенты могли понять, что конкретно произошло с запросом. Номенклатура статус-кодов HTTP действительно подробно описывает почти любые проблемы, которые могут случиться с HTTP-запросом: недопустимые значения `Accept-*`-заголовков, отсутствующий `Content-Length`, неподдерживаемый HTTP-метод, слишком длинный URI и так далее.
|
В основе технологии статус-кодов лежит понятное желание сделать ошибки машиночитаемыми, так, чтобы все промежуточные агенты могли понять, что конкретно произошло с запросом. Номенклатура статус-кодов HTTP действительно подробно описывает почти любые проблемы, которые могут случиться с HTTP-запросом: недопустимые значения `Accept-*`-заголовков, отсутствующий `Content-Length`, неподдерживаемый HTTP-метод, слишком длинный URI и так далее.
|
||||||
|
|
||||||
**NB**: обратите внимание на проблему дизайна спецификации. По умолчанию все `4xx` коды не кэшируются, за исключением: `404`, `405`, `410`, `414`. Мы не сомневаемся, что это было сделано из благих намерений, но подозреваем, что множество людей, знающих об этих тонкостях, примерно совпадает с множеством редакторов спецификации HTTP.
|
|
||||||
|
|
||||||
К сожалению, для описаний ошибок, возникающих в бизнес-логике, номенклатура статус-кодов HTTP совершенно недостаточна и вынуждает использовать статус-коды в нарушение стандарта и/или обогащать ответ дополнительной информацией об ошибке. Проблемы имплементации системы ошибок в HTTP API мы обсудим подробнее в главе «Работа с ошибками в HTTP API».
|
К сожалению, для описаний ошибок, возникающих в бизнес-логике, номенклатура статус-кодов HTTP совершенно недостаточна и вынуждает использовать статус-коды в нарушение стандарта и/или обогащать ответ дополнительной информацией об ошибке. Проблемы имплементации системы ошибок в HTTP API мы обсудим подробнее в главе «Работа с ошибками в HTTP API».
|
||||||
|
|
||||||
|
**NB**: обратите внимание на проблему дизайна спецификации. По умолчанию все `4xx` коды не кэшируются, за исключением: `404`, `405`, `410`, `414`. Мы не сомневаемся, что это было сделано из благих намерений, но подозреваем, что множество людей, знающих об этих тонкостях, примерно совпадает с множеством редакторов спецификации HTTP.
|
||||||
|
|
||||||
#### Важное замечание о кэшировании
|
#### Важное замечание о кэшировании
|
||||||
|
|
||||||
Кэширование — исключительно важная часть любой современной микросервисной архитектуры, и велик соблазн управлять им на уровне протокола — благо, стандарт предоставляет весьма функциональную и продвинутую функциональность работы с кэшами. Однако автор этой книги должен предостеречь читателя: если вы планируете имплементировать такую логику, прочитайте стандарт очень внимательно. Неверное толкование тех или иных параметров кэширования может приводить к крайне неприятным ситуациям; например, в практике автора был случай, когда случайное удаление настроек для определённой географической области (эндпойнт начал возвращать `404`) привело к неработоспособности сервиса на протяжении нескольких часов, поскольку разработчики протокола не учли, что статус `404` по умолчанию кэшируется, и клиенты просто не запрашивают новую версию настроек, пока не истечёт время жизни кэша.
|
Кэширование — исключительно важная часть любой современной микросервисной архитектуры, и велик соблазн управлять им на уровне протокола — благо, стандарт предоставляет весьма функциональную и продвинутую функциональность работы с кэшами. Однако автор этой книги должен предостеречь читателя: если вы планируете имплементировать такую логику, прочитайте стандарт очень внимательно. Неверное толкование тех или иных параметров кэширования может приводить к крайне неприятным ситуациям; например, в практике автора был случай, когда случайное удаление настроек для определённой географической области (эндпойнт начал возвращать `404`) привело к неработоспособности сервиса на протяжении нескольких часов, поскольку разработчики протокола не учли, что статус `404` по умолчанию кэшируется, и клиенты просто не запрашивают новую версию настроек, пока не истечёт время жизни кэша.
|
||||||
@ -150,15 +150,15 @@ HTTP-глагол определяет два важных свойства HTTP
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Возможны и более экзотические варианты: размещение параметра в схеме запроса или `Content-Type` ответа.
|
Возможны и более экзотические варианты: размещение параметра в схеме запроса или заголовке `Content-Type`.
|
||||||
|
|
||||||
Однако при перемещении параметра между различными составляющими запроса мы столкнёмся с тремя неприятными явлениями:
|
Однако при перемещении параметра между различными составляющими запроса мы столкнёмся с тремя неприятными явлениями:
|
||||||
* некоторые виды параметров чувствительны к регистру (путь, query-параметры, имена полей в JSON), некоторые нет (домен, имена заголовков);
|
* некоторые значения чувствительны к регистру (путь, query-параметры, имена полей в JSON), некоторые нет (домен, имена заголовков);
|
||||||
* при этом со *значениями* заголовков и вовсе неразбериха: часть из них по стандарту обязательно нечувствительна к регистру (в частности, `Content-Type`), а часть, напротив, обязательно чувствительна (например, `ETag`);
|
* при этом со *значениями* заголовков и вовсе неразбериха: часть из них по стандарту обязательно нечувствительна к регистру (в частности, `Content-Type`), а часть, напротив, обязательно чувствительна (например, `ETag`);
|
||||||
* наборы допустимых символов и правила экранирования также различны для разных частей запроса
|
* наборы допустимых символов и правила экранирования также различны для разных частей запроса
|
||||||
* для path, например, стандарта экранирования символов `/`, `?` и `#` не существует;
|
* для path, например, стандарта экранирования символов `/`, `?` и `#` не существует;
|
||||||
|
* символы unicode могут использоваться в доменных именах (хотя эта функциональность не везде поддерживается) только через своеобразную технику кодирования под названием [Punycode](https://www.rfc-editor.org/rfc/rfc3492.txt);
|
||||||
* для разных частей запросов используется разный кейсинг:
|
* для разных частей запросов используется разный кейсинг:
|
||||||
|
|
||||||
* `kebab-case` для домена, заголовков и пути;
|
* `kebab-case` для домена, заголовков и пути;
|
||||||
* `snake_case` для query-параметров;
|
* `snake_case` для query-параметров;
|
||||||
* `snake_case` или `camelCase` для тела запроса;
|
* `snake_case` или `camelCase` для тела запроса;
|
||||||
@ -169,4 +169,4 @@ HTTP-глагол определяет два важных свойства HTTP
|
|||||||
|
|
||||||
Короче говоря, ситуация с кейсингом настолько плоха и запутанна, что консистентного и удобного решения попросту нет. В этой книге мы придерживаемся следующего правила: токены даются в том кейсинге, который является общепринятым для той секции запроса, в которой находится токен; если положение токена меняется, то меняется и кейсинг. (Мы далеки от того, чтобы рекомендовать этот подход всюду; наша общая рекомендация, скорее — не умножать энтропию и пытаться минимизировать такого рода коллизии.)
|
Короче говоря, ситуация с кейсингом настолько плоха и запутанна, что консистентного и удобного решения попросту нет. В этой книге мы придерживаемся следующего правила: токены даются в том кейсинге, который является общепринятым для той секции запроса, в которой находится токен; если положение токена меняется, то меняется и кейсинг. (Мы далеки от того, чтобы рекомендовать этот подход всюду; наша общая рекомендация, скорее — не умножать энтропию и пытаться минимизировать такого рода коллизии.)
|
||||||
|
|
||||||
**NB**. Вообще говоря, JSON исходно — это JavaScript Object Notation, а в языке JavaScript кейсинг по умолчанию - `camelCase`. Мы, тем не менее, позволим себе утверждать, что JSON давно перестал быть форматом данных, привязанным к JavaScript, и в настоящее время используется для организации взаимодействия агентов, реализованных на любых языках программирования. Использование `snake_case`, по крайней мере, позволяет легко перебрасывать параметр из query в тело и обратно, что, обычно, является наиболее частотным кейсом при разработке HTTP API. Впрочем, обратный вариант (использование `camelCase` в именах query-параметров) тоже допустим.
|
**NB**: вообще говоря, JSON исходно — это JavaScript Object Notation, а в языке JavaScript кейсинг по умолчанию - `camelCase`. Мы, тем не менее, позволим себе утверждать, что JSON давно перестал быть форматом данных, привязанным к JavaScript, и в настоящее время используется для организации взаимодействия агентов, реализованных на любых языках программирования. Использование `snake_case`, по крайней мере, позволяет легко перебрасывать параметр из query в тело и обратно, что, обычно, является наиболее частотным кейсом при разработке HTTP API. Впрочем, обратный вариант (использование `camelCase` в именах query-параметров) тоже допустим.
|
Loading…
x
Reference in New Issue
Block a user