1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-05-13 21:26:26 +02:00

proofreading

This commit is contained in:
Sergey Konstantinov 2023-05-23 00:18:58 +03:00
parent 5447233eca
commit f946ffb2d3
4 changed files with 62 additions and 61 deletions

View File

@ -1,31 +1,32 @@
### [Describing Final Interfaces][api-design-describing-interfaces]
When all entities, their responsibilities, and their relations to each other are defined, we proceed to the development of the API itself. We are to describe the objects, fields, methods, and functions nomenclature in detail. In this chapter, we're giving purely practical advice on making APIs usable and understandable.
When all entities, their responsibilities, and their relations to each other are defined, we proceed to the development of the API itself. We need to describe the objects, fields, methods, and functions nomenclature in detail. In this chapter, we provide practical advice on making APIs usable and understandable.
One of the most important tasks for an API developer is to make code that other developers write using the API easily readable and maintainable. Remember that the big numbers law always works against you: if some concept or call signature might be understood wrongly, they will be wrongly understood by the increasing number of partners as the API popularity grows.
One of the most important tasks for an API developer is to ensure that code written by other developers using the API is easily readable and maintainable. Remember that the law of large numbers always works against you: if a concept or call signature can be misunderstood, it will be misunderstood by an increasing number of partners as the API's popularity grows.
**NB**: the examples in this chapter are meant to illustrate the consistency and readability problems that arise during API development. We're not giving specific advice regarding designing REST API (such advice will be given in the corresponding section of this book) or programming languages standard libraries; it's about the idea, not specific syntax.
**NB**: the examples in this chapter are meant to illustrate the consistency and readability problems that arise during API development. We do not provide specific advice on designing REST APIs (such advice will be given in the corresponding section of this book) or programming languages' standard libraries. The focus is o the idea, not specific syntax.
An important assertion at number 0:
##### 0. Rules Must Not Be Applied Unthinkingly
Rules are just simply formulated generalizations from one's experience. They are not to be applied unconditionally, and they don't make thinking redundant. Every rule has a rational reason to exist. If your situation doesn't justify following the rule — then you shouldn't do it.
Rules are simply formulated generalizations based on one's experience. They are not to be applied unconditionally, and they do not make thinking redundant. Every rule has a rational reason to exist. If your situation does not justify following a rule, then you should not do it.
This idea applies to every concept listed below. If you get an unusable, bulky, unobvious API because you follow the rules, it's a motivation to revise the rules (or the API).
This idea applies to every concept listed below. If you end up with an unusable, bulky, or non-obvious API because you followed the rules, it's a motivation to revise the rules (or the API).
It is important to understand that you can always introduce concepts of your own. For example, some frameworks willfully reject paired `set_entity` / `get_entity` methods in a favor of a single `entity()` method, with an optional argument. The crucial part is being systematic in applying the concept. If it's rendered into life, you must apply it to every single API method, or at the very least elaborate a naming rule to discern such polymorphic methods from regular ones.
It is important to understand that you can always introduce your own concepts. For example, some frameworks intentionally reject paired `set_entity` / `get_entity` methods in favor of a single `entity()` method with an optional argument. The crucial part is being systematic in applying the concept. If it is implemented, you must apply it to every single API method or at the very least develop a naming rule to distinguish such polymorphic methods from regular ones.
##### Explicit Is Always Better Than Implicit
Entity name must explicitly tell what the entity does and what side effects to expect while using it.
The entity name should explicitly indicate what the entity does and what side effects to expect when using it.
**Bad**:
```
// Cancels an order
order.canceled = true;
```
It's unobvious that a state field might be set, and that this operation will cancel the order.
It is not obvious that a state field might be modified, and that this operation will cancel the order.
**Better**:
```
@ -39,7 +40,7 @@ order.cancel();
// since the beginning of time
orders.getStats()
```
Even if the operation is non-modifying but computationally expensive, you should explicitly indicate that, especially if clients got charged for computational resource usage. Even more so, default values must not be set in a manner leading to maximum resource consumption.
Even if the operation is non-modifying but computationally expensive, you should explicitly indicate that, especially if clients are charged for computational resource usage. Furthermore, default values should not be set in a way that leads to maximum resource consumption.
**Better**:
```
@ -52,21 +53,21 @@ orders.calculateAggregatedStats({
});
```
**Try to design function signatures to be absolutely transparent about what the function does, what arguments it takes, and what's the outcome**. While reading code that works with your API, it must be easy to understand what it does without referring to the docs.
**Try to design function signatures that are transparent about what the function does, what arguments it takes, and what the outcome is**. When reading code that works with your API, it should be easy to understand what it does without referring to the documentation.
Two important implications:
**1.1.** If the operation is modifying, it must be obvious from the signature. In particular, there might be no modifying operations named `getSomething` or using the `GET` HTTP verb.
**1.1.** If the operation is modifying, it must be obvious from the signature. In particular, there should not be modifying operations named `getSomething` or using the `GET` HTTP verb.
**1.2.** If your API's nomenclature contains both synchronous and asynchronous operations, then (a)synchronicity must be apparent from signatures, **or** a naming convention must exist.
##### Specify Which Standards Are Used
Regretfully, humanity is unable to agree on the most trivial things, like which day starts the week, to say nothing about more sophisticated standards.
Regrettably, humanity is unable to agree on even the most trivial things, like which day starts the week, let alone more sophisticated standards.
So *always* specify exactly which standard is applied. Exceptions are possible if you're 100% sure that only one standard for this entity exists in the world, and every person on Earth is totally aware of it.
Therefore, *always* specify exactly which standard is being used. Exceptions are possible if you are 100% sure that only one standard for this entity exists in the world and every person on Earth is totally aware of it.
**Bad**: `"date": "11/12/2020"` — there are tons of date formatting standards; you can't even tell which number means the day number and which number means the month.
**Bad**: `"date": "11/12/2020"` — there are numerous date formatting standards. It is unclear which number represents the day and which number represents the month.
**Better**: `"iso_date": "2020-11-12"`.
@ -83,13 +84,13 @@ So *always* specify exactly which standard is applied. Exceptions are possible i
One particular implication of this rule is that money sums must *always* be accompanied by a currency code.
It is also worth saying that in some areas the situation with standards is so spoiled that, whatever you do, someone got upset. A “classical” example is geographical coordinates order (latitude-longitude vs longitude-latitude). Alas, the only working method of fighting frustration there is the Serenity Notepad to be discussed in [the corresponding chapter](#back-compat-serenity-notepad).
It is also worth mentioning that in some areas the situation with standards is so spoiled that no matter what you do, someone will be upset. A “classical” example is the order of geographical coordinates (latitude-longitude vs longitude-latitude). Unfortunately, the only effective method to address the frustration in such cases is the Serenity Notepad which will be discussed in [the corresponding chapter](#back-compat-serenity-notepad).
##### Entities Must Have Concrete Names
Avoid single amoeba-like words, such as “get,” “apply,” “make,” etc.
Avoid using single amoeba-like words, such as “get,” “apply,” “make,” etc.
**Bad**: `user.get()`hard to guess what is actually returned.
**Bad**: `user.get()`it is difficult to guess what is actually returned.
**Better**: `user.get_id()`.
@ -97,7 +98,7 @@ Avoid single amoeba-like words, such as “get,” “apply,” “make,” etc.
In the 21st century, there's no need to shorten entities' names.
**Bad**: `order.getTime()` — unclear, what time is actually returned: order creation time, order preparation time, order waiting time?…
**Bad**: `order.getTime()`it is unclear what time is actually returned: order creation time, order preparation time, order waiting time, or something else.
**Better**: `order.getEstimatedDeliveryTime()`.
@ -109,7 +110,7 @@ In the 21st century, there's no need to shorten entities' names.
strpbrk (str1, str2)
```
Possibly, an author of this API thought that the `pbrk` abbreviation would mean something to readers; clearly mistaken. Also, it's hard to tell from the signature which string (`str1` or `str2`) stands for a character set.
Possibly, the author of this API thought that the abbreviation `pbrk` would mean something to readers, but that is clearly mistaken. It is also hard to understand from the signature which string (`str1` or `str2`) represents a character set.
**Better**:
```
@ -118,30 +119,30 @@ str_search_for_characters(
lookup_character_set
)
```
— though it's highly disputable whether this function should exist at all; a feature-rich search function would be much more convenient. Also, shortening a `string` to an `str` bears no practical sense, regretfully being a routine in many subject areas.
— though it is highly debatable whether this function should exist at all; a feature-rich search function would be much more convenient. Also, shortening a `string` to `str` bears no practical sense, unfortunately being a common practice in many subject areas.
**NB**: sometimes field names are shortened or even omitted (e.g., a heterogenous array is passed instead of a set of named fields) to lessen the amount of traffic. In most cases, this is absolutely meaningless as usually the data is compressed at the protocol level.
**NB**: sometimes field names are shortened or even omitted (e.g., a heterogeneous array is passed instead of a set of named fields) to reduce the amount of traffic. In most cases, this is absolutely meaningless as the data is usually compressed at the protocol level.
##### Naming Implies Typing
A field named `recipe` must be of a `Recipe` type. A field named `recipe_id` must contain a recipe identifier that we could find within the `Recipe` entity.
A field named `recipe` must be of type `Recipe`. A field named `recipe_id` must contain a recipe identifier that can be found within the `Recipe` entity.
Same for basic types. Arrays must be named in a plural form or as collective nouns, e.g., `objects`, `children`. If that's impossible, better add a prefix or a postfix to avoid doubt.
The same applies to basic types. Arrays must be named in the plural form or as collective nouns, e.g., `objects`, `children`. If it is not possible, it is better to add a prefix or a postfix to avoid ambiguity.
**Bad**: `GET /news` — unclear whether a specific news item is returned, or a list of them.
**Bad**: `GET /news`it is unclear whether a specific news item is returned, or a list of them.
**Better**: `GET /news-list`.
Similarly, if a Boolean value is expected, entity naming must describe some qualitative state, e.g., `is_ready`, `open_now`.
Similarly, if a Boolean value is expected, entity naming must describe a qualitative state, e.g., `is_ready`, `open_now`.
**Bad**: `"task.status": true`
— statuses are not explicitly binary; also such API isn't extendable.
— statuses are not explicitly binary. Additionally, such an API is not extendable.
**Better**: `"task.is_finished": true`.
Specific platforms imply specific additions to this rule depending on the first-class citizen types they provide. For example, JSON doesn't have a `Date` object type, so the dates are to be passed as numbers or strings. In this case, it's convenient to mark dates somehow, for example, by adding `_at` or `_date` postfixes, i.e. `created_at`, `occurred_at`.
Specific platforms imply specific additions to this rule depending on the first-class citizen types they provide. For example, JSON doesn't have a `Date` object type, so dates are typically passed as numbers or strings. In this case, it's convenient to mark dates somehow, for example, by adding `_at` or `_date` postfixes, i.e. `created_at`, `occurred_at`.
If an entity name is a polysemantic term itself, which could confuse developers, better add an extra prefix or postfix to avoid misunderstanding.
If an entity name is a polysemantic term itself, which could confuse developers, it is better to add an extra prefix or postfix to avoid misunderstanding.
**Bad**:
@ -151,7 +152,7 @@ If an entity name is a polysemantic term itself, which could confuse developers,
GET /coffee-machines/{id}/functions
```
The word “function” is many-valued. It could mean built-in functions, but also “a piece of code,” or a state (machine is functioning).
The word “function” is ambiguous. It might refer to built-in functions, but it could also mean “a piece of code,” or a state (machine is functioning).
**Better**:
```
@ -162,7 +163,7 @@ GET /v1/coffee-machines/{id}⮠
##### Matching Entities Must Have Matching Names and Behave Alike
**Bad**: `begin_transition` / `stop_transition`
`begin` and `stop` terms don't match; developers will have to dig into the docs.
The terms `begin` and `stop` don't match; developers will have to refer to the documentation to find a paired method.
**Better**: either `begin_transition` / `end_transition` or `start_transition` / `stop_transition`.
@ -178,21 +179,21 @@ str_replace(needle, replace, haystack)
```
Several rules are violated:
* underscore usage is not consistent;
* functionally close methods have different `needle`/`haystack` argument ordering;
* the first function finds the first occurrence while the second one finds them all, and there is no way to deduce that fact out of the function signatures.
* the usage of an underscore is not consistent
* functionally close methods have different `needle`/`haystack` argument ordering
* the first function finds the first occurrence while the second one finds all occurrences, and there is no way to deduce that fact from the function signatures.
We're leaving the exercise of making these signatures better for the reader.
Improving these function signatures is left as an exercise for the reader.
##### Avoid Double Negations
**Bad**: `"dont_call_me": false`
— humans are bad at perceiving double negation; make mistakes.
— humans are bad at perceiving double negation and can make mistakes.
**Better**: `"prohibit_calling": true` or `"avoid_calling": true`
it's easier to read, though you shouldn't deceive yourself. Avoid semantical double negations, even if you've found a “negative” word without a “negative” prefix.
this is easier to read. However, you should not deceive yourself: it is still a double negation, even if you've found a “negative” word without a “negative” prefix.
Also worth mentioning is that making mistakes in [de Morgan's laws](https://en.wikipedia.org/wiki/De_Morgan's_laws) usage is even simpler. For example, if you have two flags:
It is also worth mentioning that mistakes in using [De Morgan's laws](https://en.wikipedia.org/wiki/De_Morgan's_laws) are even more common. For example, if you have two flags:
```
GET /coffee-machines/{id}/stocks
@ -203,7 +204,7 @@ GET /coffee-machines/{id}/stocks
}
```
“Coffee might be prepared” condition would look like `has_beans && has_cup` — both flags must be true. However, if you provide the negations of both flags:
The condition “coffee might be prepared” would look like `has_beans && has_cup` — both flags must be true. However, if you provide the negations of both flags:
```
{
@ -212,11 +213,11 @@ GET /coffee-machines/{id}/stocks
}
```
— then developers will have to evaluate the `!beans_absence && !cup_absence` flag which is equivalent to the `!(beans_absence || cup_absence)` condition. In this transition, people tend to make mistakes. Avoiding double negations helps little, and regretfully only general piece of advice could be given: avoid the situations when developers have to evaluate such flags.
— then developers will have to evaluate the `!beans_absence && !cup_absence` flag which is equivalent to the `!(beans_absence || cup_absence)` condition. In this transition, people tend to make mistakes. Avoiding double negations helps to some extent, but the best advice is to avoid situations where developers have to evaluate such flags.
##### Avoid Implicit Type Casting
This advice is opposite to the previous one, ironically. When developing APIs you frequently need to add a new optional field with a non-empty default value. For example:
This advice contradicts the previous one, ironically. When developing APIs you frequently need to add a new optional field with a non-empty default value. For example:
```
const orderParams = {
@ -227,7 +228,7 @@ const order = api.createOrder(
);
```
This new `contactless_delivery` option isn't required, but its default value is `true`. A question arises: how developers should discern explicit intention to abolish the option (`false`) from knowing not it exists (the field isn't set)? They have to write something like:
This new `contactless_delivery` option isn't required, but its default value is `true`. A question arises: how should developers discern the explicit intention to disable the option (`false`) from not knowing if it exists (the field isn't set)? They would have to write something like:
```
if (
@ -240,7 +241,7 @@ if (
}
```
This practice makes the code more complicated, and it's quite easy to make mistakes, which will effectively treat the field in an opposite manner. The same could happen if some special values (e.g., `null` or `-1`) to denote value absence are used.
This practice makes the code more complicated, and it's quite easy to make mistakes resulting in effectively treating the field as the opposite. The same can happen if special values (e.g., `null` or `-1`) are used to denote value absence.
If the protocol does not support resetting to default values as a first-class citizen, the universal rule is to make all new Boolean flags false by default.
@ -254,7 +255,7 @@ const order = api.createOrder(
);
```
If a non-Boolean field with specially treated value absence is to be introduced, then introduce two fields.
If a non-Boolean field with a specially treated absence of value is to be introduced, then introduce two fields.
**Bad**:
```
@ -293,44 +294,44 @@ POST /v1/users
}
```
**NB**: the contradiction with the previous rule lies in the necessity of introducing “negative” flags (the “no limit” flag), which we had to rename to `abolish_spending_limit`. Though it's a decent name for a negative flag, its semantics is still unobvious, and developers will have to read the docs. This is the way.
**NB**: the contradiction with the previous rule lies in the necessity of introducing “negative” flags (the “no limit” flag), which we had to rename to `abolish_spending_limit`. Though it's a decent name for a negative flag, its semantics is still not obvious, and developers will have to read the documentation. This is the way.
##### Declare Technical Restrictions Explicitly
Every field in your API comes with restrictions: the maximum allowed text length, the size of attached documents, the allowed ranges for numeric values, etc. Often, describing those limits is neglected by API developers — either because they consider it obvious, or because they simply don't know the boundaries themselves. This is of course an antipattern: not knowing what are the limits automatically implies that partners' code might stop working at any moment because of the reasons they don't control.
Every field in your API comes with restrictions: the maximum allowed text length, the size of attached documents, the allowed ranges for numeric values, etc. Often, describing those limits is neglected by API developers — either because they consider it obvious, or because they simply don't know the boundaries themselves. This is of course an antipattern: not knowing the limits automatically implies that partners' code might stop working at any moment due to reasons they don't control.
Therefore, first, declare the boundaries for every field in the API without any exceptions, and, second, generate proper machine-readable errors describing which exact boundary was violated should such a violation occur.
Therefore, first, declare the boundaries for every field in the API without any exceptions, and, second, generate proper machine-readable errors describing the exact boundary that was violated should such a violation occur.
The same reasoning applies to quotas as well: partners must have access to the statistics on which part of the quota they have already used, and the errors in the case of exceeding quotas must be informative.
#### All Requests Must Be Limited
##### All Requests Must Be Limited
The restrictions should apply not only to field sizes, but to list sizes or aggregation intervals as well.
The restrictions should apply not only to field sizes but also to list sizes or aggregation intervals.
**Bad**: `getOrders()` — what if a user made a million of orders?
**Bad**: `getOrders()` — what if a user made a million orders?
**Better**: `getOrders({ limit, parameters })` — there must be a cap to the amount of processed and returned data, and therefor a possibility to refine the query if a partner needs more data than allowed to return in one request.
**Better**: `getOrders({ limit, parameters })` — there must be a cap on the amount of processed and returned data. This also implies providing the possibility to refine the query if a partner needs more data than what is allowed to be returned in one request.
##### Describe the Retry Policy
One of the most significant performance-related challenges that nearly any API developer encounters, regardless of whether the API is an internal or a public one, is service denial due to a flood of re-requests. Temporary backend API issues, such as increased response times, can lead to complete server failure if clients begin rapidly repeating requests because of receiving an error or getting a timeout, thus generating a significantly larger than usual workload in a short amount of time.
One of the most significant performance-related challenges that nearly any API developer encounters, regardless of whether the API is internal or public, is service denial due to a flood of re-requests. Temporary backend API issues, such as increased response times, can lead to complete server failure if clients rapidly repeat requests after receiving an error or a timeout, resulting in generating a significantly larger workload than usual in a short period of time.
The best practice in such a situation is to require clients to retry API endpoints with increasing intervals (for example, the first retry occurs after one second, the second after two seconds, the third after four seconds, and so on, but no longer than one minute). Of course, in the case of a public API, no one is obliged to comply with such a requirement, but its presence certainly won't make things worse for you. At the very least, some partners will read the documentation and follow your recommendations.
The best practice in such a situation is to require clients to retry API endpoints with increasing intervals (for example, the first retry occurs after one second, the second after two seconds, the third after four seconds, and so on, up to a maximum of, let's say, one minute). Of course, in the case of a public API, no one is obliged to comply with such a requirement, but its presence certainly won't make things worse for you. At the very least, some partners will read the documentation and follow your recommendations.
Moreover, you can develop a reference implementation of the retry policy in your public SDKs and check it's correctly implemented in open-source modules to your API.
Moreover, you can develop a reference implementation of the retry policy in your public SDKs and ensure it is correctly implemented in open-source modules for your API.
##### Count the Amount of Traffic
Nowadays the amount of traffic is rarely taken into account the Internet connection is considered unlimited almost universally. However, it's still not entirely unlimited: with some degree of carelessness, it's always possible to design a system generating the amount of traffic that is uncomfortable even for modern networks.
Nowadays the amount of traffic is rarely taken into account as the Internet connection is considered unlimited almost universally. However, it is not entirely unlimited: with some degree of carelessness, it's always possible to design a system that generates an uncomfortable amount of traffic even for modern networks.
There are three obvious reasons for inflating network traffic:
* clients query for the data too frequently or cache them too little
* clients query the data too frequently or cache it too little
* no data pagination is provided
* no limits on the data fields set, or too large binary data (graphics, audio, video, etc.) is being transmitted.
* no limits are set on the data fields, or too large binary data (graphics, audio, video, etc.) is transmitted.
All these problems must be solved with setting limitations on field sizes and properly decomposing endpoints. If some entity comprises both “lightweight” data (let's say, the name and the description of the recipe) and “heavy” data (let's say, the promo picture of the beverage which might easily be a hundred times larger than the text fields), it's better to split endpoints and pass only a reference to the “heavy” data (a link to the image, in our case) — this will allow at least setting different cache policies for different kinds of data.
All these problems must be addressed by setting limitations on field sizes and properly decomposing endpoints. If an entity comprises both “lightweight” data (such as the name and description of a recipe) and “heavy” data (such as the promotional picture of a beverage which might easily be a hundred times larger than the text fields), it's better to split endpoints and pass only a reference to the “heavy” data (e.g., a link to the image). This will also allow for setting different cache policies for different kinds of data.
As a useful exercise, try modeling the typical lifecycle of a partner's app's main functionality (for example, making a single order) to count the number of requests and the amount of traffic that it takes. It might turn out that the reason for the increased amount of requests / network traffic consumption was a mistake made in the design of state change notification endpoints. We will discuss this issue in detail in the “[Bidirectional Data Flow](#api-patterns-push-vs-poll)” chapter of “The API Patterns” section of this book.
As a useful exercise, try modeling the typical lifecycle of a partner's app's main functionality (e.g., making a single order) to count the number of requests and the amount of traffic it requires. It might turn out that the high number of requests or increased network traffic consumption is due to a mistake in the design of state change notification endpoints. We will discuss this issue in detail in the “[Bidirectional Data Flow](#api-patterns-push-vs-poll)” chapter of “The API Patterns” section of this book.
##### No Results Is a Result

View File

@ -86,7 +86,7 @@ Now, let's consider a scenario where the partner receives an error from the API
}
```
**NB**: in the code sample above, we provide the “right” retry policy with exponentially increasing delays and a total limit on the number of retries, as it should be implemented in SDKs. However, be warned that real partners' code may frequently lack such precautions. For the sake of readability, we will skip this bulky construct in the following code samples.
**NB**: in the code sample above, we provide the “right” retry policy with exponentially increasing delays and a total limit on the number of retries, as we recommended earlier in the “[Describing Final Interfaces](#api-design-describing-interfaces)” chapter. However, be warned that real partners' code may frequently lack such precautions. For the sake of readability, we will skip this bulky construct in the following code samples.
2. Retrying only failed sub-requests:
```

View File

@ -215,7 +215,7 @@ GET /coffee-machines/{id}/stocks
— то разработчику потребуется вычислить флаг `!beans_absence && !cup_absence`, что эквивалентно `!(beans_absence || cup_absence)`, а вот в этом переходе ошибиться очень легко, и избегание двойных отрицаний помогает слабо. Здесь, к сожалению, есть только общий совет «избегайте ситуаций, когда разработчику нужно вычислять такие флаги».
#### Избегайте неявного приведения типов
##### Избегайте неявного приведения типов
Этот совет парадоксально противоположен предыдущему. Часто при разработке API возникает ситуация, когда добавляется новое необязательное поле с непустым значением по умолчанию. Например:

View File

@ -84,7 +84,7 @@ POST /v1/orders/bulk-status-change
}
```
**NB**: в примере выше мы приводим «правильную» политику перезапросов (с экспоненциально растущим периодом ожидания и лимитом на количество попыток), как это следует реализовать в SDK. Следует, однако, иметь в виду, что в реальном коде партнёров с большой долей вероятности ничего подобного реализовано не будет. В дальнейших примерах эту громоздкую конструкцию мы также будем опускать, чтобы упростить чтение кода.
**NB**: в примере выше мы приводим «правильную» политику перезапросов (с экспоненциально растущим периодом ожидания и лимитом на количество попыток), как мы ранее рекомендовали в главе «[Описание конечных интерфейсов](#api-design-describing-interfaces. Следует, однако, иметь в виду, что в реальном коде партнёров с большой долей вероятности ничего подобного реализовано не будет. В дальнейших примерах эту громоздкую конструкцию мы также будем опускать, чтобы упростить чтение кода.
2. Повтор только неудавшихся подзапросов:
```