@@ -598,7 +598,7 @@ ul.references li p a.back-anchor { This book is distributed under the Creative Commons Attribution-NonCommercial 4.0 International licence. Source code available at github.com/twirl/The-API-Book - Share: facebook · twitter · linkedin · redditTable of ContentsIntroductionChapter 1. On the Structure of This BookChapter 2. The API DefinitionChapter 3. Overview of Existing API Development SolutionsChapter 4. API Quality CriteriaChapter 5. The API-First ApproachChapter 6. On Backward CompatibilityChapter 7. On VersioningChapter 8. Terms and Notation KeysSection I. The API DesignChapter 9. The API Contexts PyramidChapter 10. Defining an Application FieldChapter 11. Separating Abstraction LevelsChapter 12. Isolating Responsibility AreasChapter 13. Describing Final InterfacesChapter 14. Annex to Section I. Generic API ExampleSection II. The API PatternsChapter 15. On Design Patterns in the API ContextChapter 16. Authenticating Partners and Authorizing API CallsChapter 17. Synchronization StrategiesChapter 18. Eventual ConsistencyChapter 19. Asynchronicity and Time ManagementChapter 20. Lists and Accessing ThemChapter 21. Bidirectional Data Flows. Push and Poll ModelsChapter 22. Multiplexing Notifications. Asynchronous Event ProcessingChapter 23. Atomicity of Bulk ChangesChapter 24. Partial UpdatesChapter 25. Degradation and PredictabilitySection III. The Backward CompatibilityChapter 26. The Backward Compatibility Problem StatementChapter 27. On the Waterline of the IcebergChapter 28. Extending through AbstractingChapter 29. Strong Coupling and Related ProblemsChapter 30. Weak CouplingChapter 31. Interfaces as a Universal PatternChapter 32. The Serenity Notepad[Work in Progress] Section IV. HTTP APIs & REST Architectural PrinciplesChapter 33. On the HTTP API Concept and TerminologyChapter 34. The REST MythChapter 35. Components of an HTTP Request and Their SemanticsChapter 36. Advantages and Disadvantages of HTTP APIsChapter 37. Organizing HTTP APIs Based on the REST PrinciplesChapter 38. Designing a Nomenclature of URLs and Applicable OperationsChapter 39. Working with HTTP API ErrorsChapter 40. Final Provisions and General Recommendations[Work in Progress] Section V. SDKs & UI LibrariesChapter 41. On the Content of This SectionChapter 42. The SDK: Problems and SolutionsChapter 43. The Code Generation PatternChapter 44. The UI ComponentsChapter 45. Decomposing UI ComponentsChapter 46. The MV* FrameworksChapter 47. The Backend-Driven UIChapter 48. Shared Resources and Asynchronous LocksChapter 49. Computed PropertiesChapter 50. ConclusionSection VI. The API ProductChapter 51. API as a ProductChapter 52. The API Business ModelsChapter 53. Developing a Product VisionChapter 54. Communicating with DevelopersChapter 55. Communicating with Business OwnersChapter 56. The API Services RangeChapter 57. The API Key Performance IndicatorsChapter 58. Identifying Users and Preventing FraudChapter 59. The Technical Means of Preventing ToS ViolationsChapter 60. Supporting customersChapter 61. The DocumentationChapter 62. The Testing EnvironmentChapter 63. Managing Expectations + Share: facebook · twitter · linkedin · redditTable of ContentsIntroductionChapter 1. On the Structure of This BookChapter 2. The API DefinitionChapter 3. Overview of Existing API Development SolutionsChapter 4. API Quality CriteriaChapter 5. The API-First ApproachChapter 6. On Backward CompatibilityChapter 7. On VersioningChapter 8. Terms and Notation KeysSection I. The API DesignChapter 9. The API Contexts PyramidChapter 10. Defining an Application FieldChapter 11. Separating Abstraction LevelsChapter 12. Isolating Responsibility AreasChapter 13. Describing Final InterfacesChapter 14. Annex to Section I. Generic API ExampleSection II. The API PatternsChapter 15. On Design Patterns in the API ContextChapter 16. Authenticating Partners and Authorizing API CallsChapter 17. Synchronization StrategiesChapter 18. Eventual ConsistencyChapter 19. Asynchronicity and Time ManagementChapter 20. Lists and Accessing ThemChapter 21. Bidirectional Data Flows. Push and Poll ModelsChapter 22. Multiplexing Notifications. Asynchronous Event ProcessingChapter 23. Atomicity of Bulk ChangesChapter 24. Partial UpdatesChapter 25. Degradation and PredictabilitySection III. The Backward CompatibilityChapter 26. The Backward Compatibility Problem StatementChapter 27. On the Waterline of the IcebergChapter 28. Extending through AbstractingChapter 29. Strong Coupling and Related ProblemsChapter 30. Weak CouplingChapter 31. Interfaces as a Universal PatternChapter 32. The Serenity NotepadSection IV. HTTP APIs & REST Architectural PrinciplesChapter 33. On the HTTP API Concept and TerminologyChapter 34. The REST MythChapter 35. Components of an HTTP Request and Their SemanticsChapter 36. Advantages and Disadvantages of HTTP APIsChapter 37. Organizing HTTP APIs Based on the REST PrinciplesChapter 38. Designing a Nomenclature of URLs. CRUD OperationsChapter 39. Working with HTTP API ErrorsChapter 40. Final Provisions and General Recommendations[Work in Progress] Section V. SDKs & UI LibrariesChapter 41. On the Content of This SectionChapter 42. The SDK: Problems and SolutionsChapter 43. The Code Generation PatternChapter 44. The UI ComponentsChapter 45. Decomposing UI ComponentsChapter 46. The MV* FrameworksChapter 47. The Backend-Driven UIChapter 48. Shared Resources and Asynchronous LocksChapter 49. Computed PropertiesChapter 50. ConclusionSection VI. The API ProductChapter 51. API as a ProductChapter 52. The API Business ModelsChapter 53. Developing a Product VisionChapter 54. Communicating with DevelopersChapter 55. Communicating with Business OwnersChapter 56. The API Services RangeChapter 57. The API Key Performance IndicatorsChapter 58. Identifying Users and Preventing FraudChapter 59. The Technical Means of Preventing ToS ViolationsChapter 60. Supporting customersChapter 61. The DocumentationChapter 62. The Testing EnvironmentChapter 63. Managing Expectations § @@ -2248,7 +2248,8 @@ X-Idempotency-Token: <token> If the author of this book were given a dollar each time he had to implement an additional security protocol invented by someone, he would be retired by now. API developers' inclination to create new signing procedures for requests or complex schemes of exchanging passwords for tokens is both obvious and meaningless. First, there is no need to reinvent the wheel when it comes to security-enhancing procedures for various operations. All the algorithms you need are already invented, just adopt and implement them. No self-invented algorithm for request signature checking can provide the same level of protection against a Man-in-the-Middle attack as a TLS connection with mutual certificate pinning. Second, assuming oneself to be an expert in security is presumptuous and dangerous. New attack vectors emerge daily, and staying fully aware of all actual threats is a full-time job. If you do something different during workdays, the security system you design will contain vulnerabilities that you have never heard about — for example, your password-checking algorithm might be susceptible to a timing attack or your webserver might be vulnerable to a request splitting attack. -Just in case: all APIs must be provided over TLS 1.2 or higher (preferably 1.3). +The OWASP Foundation compiles a list of the most common vulnerabilities in APIs every year, which we strongly recommend studying. +And just in case: all APIs must be provided over TLS 1.2 or higher (preferably 1.3). 25. Help Partners With Security It is equally important to provide interfaces to partners that minimize potential security problems for them. Bad: @@ -2678,7 +2679,10 @@ const pendingOrders = await api. There is also an important question regarding the default behavior of the server if no version token was passed. Theoretically, in this case, master data should be returned, as the absence of the token might be the result of an app crash and subsequent restart or corrupted data storage. However, this implies an additional load on the master node. Evaluating the Risks of Switching to Eventual Consistency -Let us state an important assertion: the methods of solving architectural problems we're discussing in this section are probabilistic. Abolishing strict consistency means that, even if all components of the system work perfectly, client errors will still occur — and we may only try to lessen their numbers for typical usage profiles. +Let us state an important assertion: the methods of solving architectural problems we're discussing in this section are probabilistic. Abolishing strict consistency means that even if all components of the system work perfectly, client errors will still occur. It might appear that they could be simply ignored, but in reality, doing so means introducing risks. +Imagine that because of eventual consistency, users of our API sometimes cannot create orders with their first attempt. For example, a customer adds a new payment method in the application, but their subsequent order creation request is routed to a replica that hasn't yet received the information regarding the newest payment method. As these two actions (adding a bank card and making an order) often go in conjunction, there will be a noticeable percentage of errors — let's say, 1%. At this stage, we could disregard the situation as it appears harmless: in the worst-case scenario, the client will repeat the request. +But let's go a bit further and imagine there is an error in a new version of the application, and 0.1% of end users cannot make an order at all because the client sends a wrong payment method identifier. In the absence of this 1% background noise of consistency-bound errors, we would find the issue very quickly. However, amidst this constant inflow of errors, identifying problems like this one could be very challenging as it requires configuring monitoring systems to reliably exclude the data consistency errors, and this could be very complicated or even impossible. The author of this book, in his job, has seen several situations when critical mistakes that affect a small percentage of users were not noticed for months. +Therefore, the task of proactively lowering the number of these background errors is crucially important. We may try to reduce their occurrence for typical usage profiles. NB: the “typical usage profile” stipulation is important: an API implies the variability of client scenarios, and API usage cases might fall into several groups, each featuring quite different error profiles. The classical example is client APIs (where it's an end user who makes actions and waits for results) versus server APIs (where the execution time is per se not so important — but let's say mass parallel execution might be). If this happens, it's a strong signal to make a family of API products covering different usage scenarios, as we will discuss in “The API Services Range” chapter of “The API Product” section of this book. Let's return to the coffee example, and imagine we implemented the following scheme: @@ -4491,7 +4495,7 @@ ProgramContext.dispatch = (action) => { NB. The perfect example of avoiding this anti-pattern is the development of compilers; usually, the next compiler's version is compiled with the previous compiler's version. 5. Keep a Notepad Whatever tips and tricks described in the previous chapters you use, it's often quite probable that you can't do anything to prevent API inconsistencies from piling up. It's possible to reduce the speed of this stockpiling, foresee some problems, and have some interface durability reserved for future use. But one can't foresee everything. At this stage, many developers tend to make some rash decisions, e.g., releasing a backward-incompatible minor version to fix some design flaws. -We highly recommend never doing that. Remember that the API is also a multiplier of your mistakes. What we recommend is to keep a serenity notepad — to write down the lessons learned, and not to forget to apply this knowledge when a new major API version is released.[Work in Progress] Section IV. HTTP APIs & REST Architectural PrinciplesChapter 33. On the HTTP API Concept and Terminology +We highly recommend never doing that. Remember that the API is also a multiplier of your mistakes. What we recommend is to keep a serenity notepad — to write down the lessons learned, and not to forget to apply this knowledge when a new major API version is released.Section IV. HTTP APIs & REST Architectural PrinciplesChapter 33. On the HTTP API Concept and Terminology The problem of designing HTTP APIs is unfortunately one of the most “holywar”-inspiring issues. On one hand, it is one of the most popular technologies but, on the other hand, it is quite complex and difficult to comprehend due to the large and fragmented standard split into many RFCs. As a result, the HTTP specification is doomed to be poorly understood and imperfectly interpreted by millions of software engineers and thousands of textbook writers. Therefore, before proceeding to the useful part of this Section, we must clarify exactly what we are going to discuss. It has somehow happened that the entire modern network stack used for developing client-server APIs has been unified in two important points. One of them is the Internet Protocol Suite, which comprises the IP protocol as a base and an additional layer on top of it in the form of either the TCP or UDP protocol. Today, alternatives to the TCP/IP stack are used for a very limited subset of engineering tasks. However, from a practical standpoint, there is a significant inconvenience that makes using raw TCP/IP protocols much less practical. They operate over IP addresses which are poorly suited for organizing distributed systems: @@ -4766,7 +4770,7 @@ X-ApiName-Partner-Id: <partner_id> } } -the server could have simply responded with 400 Bad Request, passing the request identifier as a custom header like X-OurCoffeeAPI-Request-Id. Nevertheless, protocol designers decided to introduce their own custom format. +the server could have simply responded with 400 Bad Request, passing the request identifier as a custom header like X-OurCoffeeAPI-RequestId. Nevertheless, protocol designers decided to introduce their own custom format. This situation (not only with JSON-RPC but with essentially every high-level protocol built on top of HTTP) has developed due to various reasons. Some of them are historical (such as the inability to use many HTTP protocol features in early implementations of the XMLHttpRequest functionality in web browsers). However, new RPC protocols relying on the bare minimum of HTTP capabilities continue to emerge today. To answer this question, we must emphasize a very important distinction between application-level protocols (such as JSON-RPC in our case) and pure HTTP. A 400 BadRequest is a transparent status for every intermediary network agent but a JSON-RPC custom error is not. Firstly, only a JSON-RPC-enabled client can read it. Secondly, and more importantly, in JSON-RPC, the request status is not metadata. In pure HTTP, the details of the operation, such as the method, requested URL, execution status, and request / response headers are readable without the necessity to parse the entire body. In most higher-level protocols, including JSON-RPC, this is not the case: even a protocol-enabled client must read a body to retrieve that information. How does an API developer benefit from the capability of reading request and response metadata? The modern client-server communication stack, as envisioned by Fielding, is multi-layered. We can enumerate a number of intermediary agents that process network requests and responses: @@ -5035,9 +5039,9 @@ Authorization: Bearer <token> This approach might be further enhanced by introducing granular permissions to carry out specific actions, access levels, additional ACL service calls, etc. -Importantly, the visible redundancy of the format ceases to exist: user_id in the request is now not duplicated in the token payload as these identifiers carry different semantics: on which resource the operation is performed against who performs it. The two often coincide, but this coincidence is just a special case. Unfortunately, this doesn't negate the fact that it's quite easy simply to forget to implement this unobvious check in the code. This is the way.Chapter 38. Designing a Nomenclature of URLs and Applicable Operations +Importantly, the visible redundancy of the format ceases to exist: user_id in the request is now not duplicated in the token payload as these identifiers carry different semantics: on which resource the operation is performed against who performs it. The two often coincide, but this coincidence is just a special case. Unfortunately, this doesn't negate the fact that it's quite easy simply to forget to implement this unobvious check in the code. This is the way.Chapter 38. Designing a Nomenclature of URLs. CRUD Operations As we noted on several occasions in the previous chapters, neither the HTTP and URL standards nor REST architectural principles prescribe concrete semantics for the meaningful parts of a URL (notably, path fragments and key-value pairs in the query). The rules for organizing URLs in an HTTP API exist only to improve the API's readability and consistency from the developers' perspective. However, this doesn't mean they are unimportant. Quite the opposite: URLs in HTTP APIs are a means of describing abstraction levels and entities' responsibility areas. A well-designed API hierarchy should be reflected in a well-designed URL nomenclature. -NB: the lack of specific guidance from the specification editors naturally led to developers inventing it themselves. Many of these spontaneous practices can be found on the Internet, such as the requirement to use only nouns in URLs. They are often claimed to be a part of the standards or REST architectural principles (which they are not). Nevertheless, deliberately ignoring such self-proclaimed “best practices” is not the best decision for an API vendor as it increases the chances of being misunderstood. +NB: the lack of specific guidance from the specification editors naturally led to developers inventing it themselves. Many of these spontaneous practices can be found on the Internet, such as the requirement to use only nouns in URLs. They are often claimed to be a part of the standards or REST architectural principles (which they are not). Nevertheless, deliberately ignoring such self-proclaimed “best practices” is a rather risky decision for an API vendor as it increases the chances of being misunderstood. Traditionally, the following semantics are considered to be the default: Path components (i.e., fragments between / symbols) are used to organize nested resources, such as /partner/{id}/coffee-machines/{id}. A path can be further extended by adding new suffixes to indicate subordinate sub-resources. @@ -5046,6 +5050,22 @@ Authorization: Bearer <token> This convention allows for reflecting almost any API's entity nomenclature decently and it is more than reasonable to follow it (and it's unreasonable to defiantly neglect it). However, this indistinctly defined logic inevitably leads to numerous variants of interpreting it: +Where do metadata of operations end and “regular” data begin, and how acceptable is it to duplicate fields in both places? For example, one common practice in HTTP APIs is to indicate the type of returned data by adding an “extension” to the URL, similar to file names in file systems (i.e., when accessing the resource /v1/orders/{id}.xml, the response will be in XML format, and when accessing /v1/orders/{id}.json, it will be in JSON format). On the one hand, Accept* headers are intended to determine the data format. On the other hand, code readability is clearly improved by introducing a “format” parameter directly in the URL. + + +As a consequence of the previous point, how should the version of the API itself be correctly indicated in an HTTP API? At least three options can be suggested, each of which fully complies with the letter of the standard: + +A path parameter: /v1/orders/{id} +A query parameter: /orders/{id}?version=1 +A header: +GET /orders/{id} HTTP/1.1 +X-OurCoffeeAPI-Version: 1 + + + +Even more exotic options can be added here, such as specifying the schema in a customized media type or request protocol. + + How exactly should the endpoints connecting two entities lacking a clear relation between them be organized? For example, how should a URL for preparing a lungo on a specific coffee machine look? /coffee-machines/{id}/recipes/lungo/prepare @@ -5070,17 +5090,27 @@ Authorization: Bearer <token> NB: the authors of the standard are also concerned about this dichotomy and have finally proposed the QUERY HTTP method, which is basically a safe (i.e., non-modifying) version of POST. However, we do not expect it to gain widespread adoption just as the existing SEARCH verb did not. -Unfortunately, we don't have simple answers to these questions. In this book, we stick to the following approach: - -Signatures must be readable and concise first and foremost. Making the code more complicated to align with some abstract concept is undesirable. -Hierarchies are indicated if they are unequivocal. If a low-level entity is a full subordinate of a higher-level entity, the relation will be expressed with nested path fragments. +Unfortunately, we don't have simple answers to these questions. Within this book, we adhere to the following approach: the call signature should, first and foremost, be concise and readable. Complicating signatures for the sake of abstract concepts is undesirable. In relation to the mentioned issues, this means that: + + +Operation metadata should not change the meaning of the operation. If a request reaches the final microservice without any headers at all, it should still be executable, although some auxiliary functionality may degrade or be absent. + + +We use versioning in the path for one simple reason: all other methods make sense only if, when changing the major version of the protocol, the URL nomenclature remains the same. However, if the resource nomenclature can be preserved, there is no need to break backward compatibility. + + +Hierarchies are indicated if they are unequivocal. If a low-level entity is a full subordinate of a higher-level entity, the relation will be expressed with nested path fragments. If there are doubts about the hierarchy persisting during further development of the API, it is more convenient to create a new root path prefix rather than employ nested paths. -For “cross-domain” operations (i.e., when it is necessary to refer to entities of different abstraction levels within one request) it is better to have a dedicated resource specifically for this operation (e.g., in the example above, we would prefer the /prepare?coffee_machine_id=<id>&recipe=lungo signature). -The semantics of the HTTP verbs take priority over false non-safety / non-idempotency warnings. Furthermore, the author of this book prefers using POST to indicate any unexpected side effects of an operation, such as high computational complexity, even if it is fully safe. - + +For “cross-domain” operations (i.e., when it is necessary to refer to entities of different abstraction levels within one request) it is better to have a dedicated resource specifically for this operation (e.g., in the example above, we would prefer the /prepare?coffee_machine_id=<id>&recipe=lungo signature). + + +The semantics of the HTTP verbs take priority over false non-safety / non-idempotency warnings. Furthermore, the author of this book prefers using POST to indicate any unexpected side effects of an operation, such as high computational complexity, even if it is fully safe. + + NB: passing variables as either query parameters or path fragments affects not only readability. Let's consider the example from the previous chapter and imagine that gateway D is implemented as a stateless proxy with a declarative configuration. Then receiving a request like this: @@ -5200,7 +5230,230 @@ Location: /v1/orders/{id} Finally, with deleting resources the situation is simple: in modern services, data is never deleted, only archived or marked as deleted. Therefore, instead of a DELETE /v1/orders/{id} endpoint there should be PUT /v1/orders/{id}/archive or PUT /v1/archive?order=<order_id>. In Conclusion The idea of CRUD as a methodology of describing typical operations applied to resources with a small set of uniform verbs quickly evolves towards a bucket of different endpoints, each of them covering some specific aspect of working with the entity during its lifecycle. -This discourse is not to be perceived as criticizing the idea of CRUD itself. We just point out that in complex subject areas cutting edges and sticking to some mnemonic rules rarely plays out. It is much better to design entity manipulation URLs based on specific use cases. And if you do want to have a uniform interface to manipulate typical entities, you would rather initially design it much more detailed and extensible than just a set of four HTTP-CRUD methods.Chapter 39. Working with HTTP API ErrorsChapter 40. Final Provisions and General Recommendations[Work in Progress] Section V. SDKs & UI LibrariesChapter 41. On the Content of This SectionChapter 42. The SDK: Problems and SolutionsChapter 43. The Code Generation PatternChapter 44. The UI ComponentsChapter 45. Decomposing UI ComponentsChapter 46. The MV* FrameworksChapter 47. The Backend-Driven UIChapter 48. Shared Resources and Asynchronous LocksChapter 49. Computed PropertiesChapter 50. ConclusionSection VI. The API ProductChapter 51. API as a Product +This discourse is not to be perceived as criticizing the idea of CRUD itself. We just point out that in complex subject areas cutting edges and sticking to some mnemonic rules rarely plays out. It is much better to design entity manipulation URLs based on specific use cases. And if you do want to have a uniform interface to manipulate typical entities, you would rather initially design it much more detailed and extensible than just a set of four HTTP-CRUD methods.Chapter 39. Working with HTTP API Errors +The examples of organizing HTTP APIs discussed in the previous chapters were mostly about “happy paths,” i.e., the direct path of working with an API in the absence of obstacles. It's now time to talk about the opposite case: how HTTP APIs should work with errors and how the standard and the REST architectural principles can help us. +Imagine that some actor (a client or a gateway) tries to create a new order: +POST /v1/orders?user_id=<user_id> HTTP/1.1 +Authorization: Bearer <token> +If-Match: <revision> + +{ /* order parameters */ } + +What problems could potentially happen while handling the request? Off the top of the mind, it might be: + +The request cannot be parsed (invalid symbols, syntax violation, etc.) +The authorization token is missing +The authorization token is invalid +The token is valid, but the user is not permitted to create new orders +The user is deleted or deactivated +The user identifier is invalid or does not exist +The revision is missing +The revision does not match the actual one +Some required fields are missing in the request body +A value of a field exceeds the allowed boundaries +The limit for the number of requests reached +The server is overloaded and cannot respond +Unknown server error (i.e., the server is broken to the extent that it's impossible to understand why the error happened). + +From general considerations, the natural idea is to assign a status code for each mistake. Obviously, the 403 Forbidden code fits well for mistake #4, and the 429 Too Many Requests for #11. However, let's not be rash and ask first for what purpose are we assigning codes to errors? +Generally speaking, there are three kinds of actors in the system: the user, the application (a client), and the server. Each of these actors needs to understand several important things about the error (and the answers could actually differ for each of them): + +Who made the mistake: the end user, the developer of the client, the backend developer, or another interim agent such as the network stack programmer? + +And let's not forget about the possibility of the mistake being deliberately made by either an end user or a client developer while trying to blunt-force hijack the account of another user. + + +Is it possible to fix the error by just repeating the request? + +If yes, then after what period of waiting? + + +If it is not the case, is it still possible to fix it by reformulating the request? +If the error cannot be resolved, what should be done about it? + +One of these questions is easily answered in the HTTP API paradigm: the desired interval of repeating the request might be indicated in a Retry-After header. Also, HTTP helps with question #1: to understand which side is the cause of the error, the first digit in the HTTP status code is used (see below). +With the other questions, the situation is unfortunately much more complicated. +Client Errors +Status codes that start with the digit 4 indicate that it was the user or the client who made a mistake, or at least the server decided so. Usually, repeating a request that resulted in a 4xx error is meaningless: the request will never be fulfilled unless some additional actions are performed. However, there are notable exceptions, most importantly 429 Too Many Requests and 404 Not Found. The latter implies some “uncertainty state” according to the standard: the server could use it if exposing the real cause of the error is undesirable. After receiving a 404, the request might be repeated, possibly yielding a different outcome. To indicate the persistent non-existence of a resource, a separate 410 Gone status is used. +A more interesting question is what the client can (or must) do if such an error is received. As we discussed in the “Isolating Responsibility Areas” chapter, if the error can be resolved, there must be a machine-readable description for the client to interpret. In the case it cannot, human-readable instructions should be provided for the user (even “Try restarting the application” is a better user experience than “Unknown error happened”) and for the client developer. +If we try to apply this principle to HTTP APIs, we will soon learn that the situation is complicated. On one hand, the protocol includes a lot of codes that indicate specific problems with using the protocol, such as 405 Method Not Allowed (indicates that the verb in the request cannot be applied to the requested resource), 406 Not Acceptable (the server cannot return a representation that satisfies the Accept* headers in the request), 411 Length Required, 414 URI Too Long, etc. The client code might process these errors and sometimes even perform some actions to mitigate them (for example, add a Content-Length header in case of a 411 error). However, this is hardly applicable to business logic. If the server returns a 429 Too Many Requests if some limit is exceeded, there are no standardized means of indicating which exact limit was hit. +Sometimes, the absence of a common approach to describing business logic errors is circumvented by using different codes with almost identical semantics (or just randomly chosen codes) to distinguish between different causes of the error. One notable example is the widely adopted usage of the 401 Unauthorized status code to indicate the absence or the invalid value of authorization headers, which is a signal for an application to ask the user to log in. This usage contradicts the standard (which requires that a 401 response must contain the WWW-Authenticate header that describes the methods of authorization; we are unaware of a single API that follows this requirement), but it has become a de facto standard itself. +Even if we choose this approach, there are very few status codes that can reflect different aspects of the same error type. In fact, we face the situation that all the multiplicity of business-bound errors is to be returned using a very limited set of status codes: + +400 Bad Request for all the errors related to request validation issues. (Some purists insist that 400 corresponds to format violations such as invalid JSON. For logical errors, the 422 Unprocessable Content code is to be used. This actually changes nothing regarding the discussed problem.) +403 Forbidden for any problems related to authorizing the user's actions. +404 Not Found if any of the entities referred to in the request are non-existent or if exposing the real cause of the error is undesirable. +409 Conflict if data integrity is violated. +410 Gone if the resource was deleted. +429 Too Many Requests if some quotas are exceeded. + +The editors of the specification are very well aware of this problem as they state that “the server SHOULD send a representation containing an explanation of the error situation, and whether it is a temporary or permanent condition.” This, however, contradicts the entire idea of a uniform machine-readable interface (and so does the idea of using arbitrary status codes). (Let us additionally emphasize that this lack of standard tools to describe business logic-bound errors is one of the reasons we consider the REST architectural style as described by Fielding in his 2008 article non-viable. The client must possess prior knowledge of error formats and how to work with them. Otherwise, it could restore its state after an error only by restarting the application.) +Additionally, there is a third dimension to this problem in the form of webserver software for monitoring system health that often relies on status codes to plot charts and emit notifications. However, two errors represented with the same status code — let's say, wrong password and expired token — might be very different. The increased rate of the former might indicate brute-forcing of accounts, while an unusually high frequency of the latter could be a result of a client error if a new version of an application wrongly caches authorization tokens. +All these observations naturally lead us to the following conclusion: if we want to use errors for diagnostics and (possibly) helping clients to recover, we need to include machine-readable metadata about the error subtype and, possibly, additional properties to the error body with a detailed description of the error. For example, as we proposed in the “Describing Final Interfaces” chapter: +POST /v1/coffee-machines/search HTTP/1.1 + +{ + "recipes": ["lngo"], + "position": { + "latitude": 110, + "longitude": 55 + } +} +→ +HTTP/1.1 400 Bad Request +X-OurCoffeeAPI-Error-Kind:⮠ + wrong_parameter_value + +{ + "reason": "wrong_parameter_value", + "localized_message": + "Something is wrong.⮠ + Contact the developer of the app." + "details": { + "checks_failed": [ + { + "field": "recipe", + "error_type": "wrong_value", + "message": + "Unknown value: 'lngo'.⮠ + Did you mean 'lungo'?" + }, + { + "field": "position.latitude", + "error_type": + "constraint_violation", + "constraints": { + "min": -90, + "max": 90 + }, + "message": + "'position.latitude' value⮠ + must fall within⮠ + the [-90, 90] interval" + } + ] + } +} + +Let us also remind the reader that the client must treat unknown 4xx status codes as a 400 Bad Request error. Therefore, the (meta)data format for the 400 error must be as general as possible. +Server Errors +5xx errors indicate that the client did everything right, and the problem is server-bound. For the client, the only important thing about the server error is whether it makes sense to repeat the request (and if yes, then when). Keeping in mind that in publicly available APIs, the real reason for the error is usually not exposed, having just the 500 Internal Server Error and 503 Service Unavailable codes is enough for most subject areas. (The latter is needed to indicate that the denial of service state is temporary and it might be replaced with just a Retry-After header to the 500 error.) +However, for internal systems, this argumentation is wrong. To build proper monitoring and notification systems, server errors must contain machine-readable error subtypes, just like the client errors. The same approaches are applicable (either using arbitrary status codes and/or passing error kind as a header); however, this data must be stripped off by a gateway that marks the border between external and internal systems and replaced with general instructions for both developers and end users, describing actions that need to be performed upon receiving an error. +POST /v1/orders/?user_id=<user id> HTTP/1.1 +If-Match: <revision> + +{ parameters } +→ +// The response the gateway received +// from the server, the metadata +// of which will be used for +// monitoring and diagnostics +HTTP/1.1 500 Internal Server Error +// Error kind: timeout from the DB +X-OurCoffeAPI-Error-Kind: db_timeout +{ /* + * Additional data, such as + * which host returned an error + */ } + +// The response as returned to +// the client. The details regarding +// the server error are removed +// and replaced with instructions +// for the client. As at the gateway +// level it is unknown whether +// order creation succeeded, the client +// is advised to repeat the request +// and/or retrieve the actual state. +HTTP/1.1 500 Internal Server Error +Retry-After: 5 + +{ + "reason": "internal_server_error", + "localized_message": "Cannot get⮠ + a response from the server.⮠ + Please try repeating the operation + or reload the page.", + "details": { + "can_be_retried": true, + "is_operation_failed": "unknown" + } +} + +However, we go on a slippery slope here. The contemporary practice of implementing HTTP API clients allows for repeating safe requests (e.g., GET, HEAD, and OPTIONS methods). In the case of unsafe methods, developers need to write code to repeat the request, and to do so they need to read the documentation very carefully to check if it is the desired behavior and if it is actually safe. +Theoretically, with idempotent PUT and DELETE it should be more convenient. Practically, as many developers let this knowledge pass them, frameworks for working with HTTP APIs will likely not repeat these requests. Still, we can get some benefit from following the standards as the signature itself indicates that the request can be retried. +As for more complex operations, to make developers aware that they can repeat a potentially unsafe operation, we could introduce a format describing the possible actions in the error response itself… However, developers seldom expect to find such instructions in the error body, probably because programmers rarely see 5xx errors during development, unlike their 4xx counterparts, and testing environments usually do not provide capabilities to emulate server errors. All in all, you will have to describe the desirable actions in the documentation. (Be aware that this instruction will likely be ignored. This is the way.) +Organizing HTTP API Error Nomenclature in Practice +As it is obvious from what was discussed above, there are essentially three approaches to working with errors in HTTP APIs: + + +Applying an “extended interpretation” to the status code nomenclature, or in plain words, selecting or inventing a new status code for each new type of error introduced. (The author of this book has frequently observed an approach to API development that included choosing a status code based on wording resembling the error cause, disregarding its description in the standard completely.) + + +Abolishing the use of status codes and developing a format for errors enveloped in a 200 HTTP response. Most RPC frameworks choose this direction. + +2a. A subvariant of this strategy is using just two status codes (400 for every client error, 500 for every server error), optionally complemented by a third one (404 to indicate situations of uncertainty). + + + +Employing a mixed approach, i.e., using status codes in accordance with their semantics to indicate an error family with additional (meta)data being passed in a specially developed format (similar to the code samples we gave above). + + +Obviously, only approach #3 could be considered compliant with the standard. Let us be honest and say that the benefits of following it (especially compared to option #2a) are not very significant and only comprise better readability of logs and transparency for intermediate proxies.Chapter 40. Final Provisions and General Recommendations +Let's summarize what was discussed in the previous chapters. To design a fine HTTP API one needs to: + +Describe a happy path, i.e. draw a diagram of all HTTP calls that occur during a normal work cycle of an application. +Interpret every call as an operation executed on a resource and assemble a nomenclature of URLs and applicable methods accordingly. +Enumerate errors that might occur during operation execution and determine paths to restore the application state for clients after receiving an error. +Decide which functionality will be communicated at the HTTP protocol level, i.e., which standard protocol capabilities to use in conjunction with what tools and software and the extent of their usage. +Develop a detailed specification regarding the aforementioned list points. +Check yourselves: elaborate on paragraphs 1-3 to write pseudo-code for the application's business logic in accordance with the specification, and evaluate the convenience, understandability and readability of your API. + +Additionally, we'd like to provide some code style advice: + + +Do not differentiate paths with trailing / and without it. Employ a default policy (we would rather recommend ending paths with / for a simple reason: it allows for referring to operations on the domain root resource in a readable manner as VERB /). If you decide to prohibit one of the variants (let's say, all URLs must end with a trailing slash), make a redirect or provide a very readable error message if a developer tries to call a URL formatted otherwise. + + +Include common headers (such as Date, Content-Type, Content-Encoding, Content-Length, Cache-Control, Retry-After, etc.) in the responses and generally avoid relying on clients to guess default protocol parameters correctly. + + +Support the OPTIONS method and the CORS protocol just in case your API needs to be accessed from a Web browser. + + +Choose a casing rule and a rule for transforming casing while moving a parameter from one part of an HTTP request to another. + + +Always leave an opportunity for backward-compatible extension of an API method. In particular, always return a JSON object as the endpoint response root as objects can always be extended with a new field, unlike arrays and primitives. + +Let us also note that an empty string is invalid JSON, so you need to return an empty object {} in 200 responses even if it doesn't have a specific meaning. Alternatively, you can use the 204 No Content status code with an empty body, which is not extensible. + + + +For every GET response, provide explicit caching parameters (otherwise, there is always a chance that a client or an intermediate agent invents them on their own). + + +Do not employ known possibilities to serve requests in violation of the standard and avoid exploiting “gray zones” of the protocol. In particular: + +Do not place unsafe operations behind the GET verb, and do not place non-idempotent operations behind the PUT / DELETE methods. +Maintain the GET / PUT / DELETE operations symmetry. +Do not allow GET / HEAD / DELETE requests to have a body and do not provide bodies in response to HEAD requests or alongside the 204 status code. +Do not invent your own standards for passing arrays and nested objects as query parameters. It is better to use an HTTP verb that allows having a body, or as a last resort pass the parameter as a base64-encoded JSON-stringified value. +Do not put parameters that require escaping (i.e., non-alphanumeric ones) in a path or a domain of a URL. Use query or body parameters for this purpose. + + + +Familiarize yourself with at least the basics of typical vulnerabilities in HTTP APIs used by attackers, such as: + +CSRF +SSRF +HTTP Response Splitting +Unvalidated Redirects and Forwards + +and include protection against these attack vectors at the webserver software level. The OWASP community provides a good cheatsheet on the best HTTP API security practices. + + +In conclusion, we would like to make the following statement: building an HTTP API is relying on the common knowledge of HTTP call semantics and drawing benefits from it by leveraging various software built upon this paradigm, from client frameworks to server gateways, and developers reading and understanding API specifications. In this sense, the HTTP ecosystem provides probably the most comprehensive vocabulary, both in terms of profoundness and adoption, compared to other technologies, allowing for describing many different situations that may arise in client-server communication. While the technology is not perfect and has its flaws, for a public API vendor, it is the default choice, and opting for other technologies rather needs to be substantiated as of today.[Work in Progress] Section V. SDKs & UI LibrariesChapter 41. On the Content of This SectionChapter 42. The SDK: Problems and SolutionsChapter 43. The Code Generation PatternChapter 44. The UI ComponentsChapter 45. Decomposing UI ComponentsChapter 46. The MV* FrameworksChapter 47. The Backend-Driven UIChapter 48. Shared Resources and Asynchronous LocksChapter 49. Computed PropertiesChapter 50. ConclusionSection VI. The API ProductChapter 51. API as a Product There are two important statements regarding APIs viewed as products. diff --git a/docs/API.en.pdf b/docs/API.en.pdf index df64146..83867dc 100644 Binary files a/docs/API.en.pdf and b/docs/API.en.pdf differ diff --git a/docs/API.ru.epub b/docs/API.ru.epub index 19eeb38..30637c4 100644 Binary files a/docs/API.ru.epub and b/docs/API.ru.epub differ diff --git a/docs/API.ru.html b/docs/API.ru.html index 42a95ac..1210846 100644 --- a/docs/API.ru.html +++ b/docs/API.ru.html @@ -577,7 +577,7 @@ ul.references li p a.back-anchor { Ссылка: - СодержаниеВведениеГлава 1. О структуре этой книгиГлава 2. Определение APIГлава 3. Обзор существующих решений в области разработки APIГлава 4. Критерии качества APIГлава 5. API-first подходГлава 6. Обратная совместимостьГлава 7. О версионированииГлава 8. Условные обозначения и терминологияРаздел I. Проектирование APIГлава 9. Пирамида контекстов APIГлава 10. Определение области примененияГлава 11. Разделение уровней абстракцииГлава 12. Разграничение областей ответственностиГлава 13. Описание конечных интерфейсовГлава 14. Приложение к разделу I. Модельный APIРаздел II. Паттерны дизайна APIГлава 15. О паттернах проектирования в контексте APIГлава 16. Аутентификация партнёров и авторизация вызовов APIГлава 17. Стратегии синхронизацииГлава 18. Слабая консистентностьГлава 19. Асинхронность и управление временемГлава 20. Списки и организация доступа к нимГлава 21. Двунаправленные потоки данных. Push и poll-моделиГлава 22. Мультиплексирование сообщений. Асинхронная обработка событийГлава 23. Атомарность массовых измененийГлава 24. Частичные обновленияГлава 25. Деградация и предсказуемостьРаздел III. Обратная совместимостьГлава 26. Постановка проблемы обратной совместимостиГлава 27. О ватерлинии айсбергаГлава 28. Расширение через абстрагированиеГлава 29. Сильная связность и сопутствующие проблемыГлава 30. Слабая связностьГлава 31. Интерфейсы как универсальный паттернГлава 32. Блокнот душевного покоя[В разработке] Раздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологииГлава 34. Мифология RESTГлава 35. Составляющие HTTP запросов и их семантикаГлава 36. Преимущества и недостатки HTTP APIГлава 37. Организация HTTP API согласно принципам RESTГлава 38. Разработка номенклатуры URL ресурсов и операций над нимиГлава 39. Работа с ошибками в HTTP APIГлава 40. Заключительные положения и общие рекомендации[В разработке] Раздел V. SDK и UIГлава 41. О содержании разделаГлава 42. SDK: проблемы и решенияГлава 43. КодогенерацияГлава 44. UI-компонентыГлава 45. Декомпозиция UI-компонентов. MV*-подходыГлава 46. MV*-фреймворкиГлава 47. Backend-Driven UIГлава 48. Разделяемые ресурсы и асинхронные блокировкиГлава 49. Вычисляемые свойстваГлава 50. В заключениеРаздел VI. API как продуктГлава 51. Продукт APIГлава 52. Бизнес-модели APIГлава 53. Формирование продуктового виденияГлава 54. Взаимодействие с разработчикамиГлава 55. Взаимодействие с бизнес-аудиториейГлава 56. Линейка сервисов APIГлава 57. Ключевые показатели эффективности APIГлава 58. Идентификация пользователей и борьба с фродомГлава 59. Технические способы борьбы с несанкционированным доступом к APIГлава 60. Поддержка пользователей APIГлава 61. ДокументацияГлава 62. Тестовая средаГлава 63. Управление ожиданиями + СодержаниеВведениеГлава 1. О структуре этой книгиГлава 2. Определение APIГлава 3. Обзор существующих решений в области разработки APIГлава 4. Критерии качества APIГлава 5. API-first подходГлава 6. Обратная совместимостьГлава 7. О версионированииГлава 8. Условные обозначения и терминологияРаздел I. Проектирование APIГлава 9. Пирамида контекстов APIГлава 10. Определение области примененияГлава 11. Разделение уровней абстракцииГлава 12. Разграничение областей ответственностиГлава 13. Описание конечных интерфейсовГлава 14. Приложение к разделу I. Модельный APIРаздел II. Паттерны дизайна APIГлава 15. О паттернах проектирования в контексте APIГлава 16. Аутентификация партнёров и авторизация вызовов APIГлава 17. Стратегии синхронизацииГлава 18. Слабая консистентностьГлава 19. Асинхронность и управление временемГлава 20. Списки и организация доступа к нимГлава 21. Двунаправленные потоки данных. Push и poll-моделиГлава 22. Мультиплексирование сообщений. Асинхронная обработка событийГлава 23. Атомарность массовых измененийГлава 24. Частичные обновленияГлава 25. Деградация и предсказуемостьРаздел III. Обратная совместимостьГлава 26. Постановка проблемы обратной совместимостиГлава 27. О ватерлинии айсбергаГлава 28. Расширение через абстрагированиеГлава 29. Сильная связность и сопутствующие проблемыГлава 30. Слабая связностьГлава 31. Интерфейсы как универсальный паттернГлава 32. Блокнот душевного покояРаздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологииГлава 34. Мифология RESTГлава 35. Составляющие HTTP запросов и их семантикаГлава 36. Преимущества и недостатки HTTP APIГлава 37. Организация HTTP API согласно принципам RESTГлава 38. Разработка номенклатуры URL ресурсов. CRUD-операцииГлава 39. Работа с ошибками в HTTP APIГлава 40. Заключительные положения и общие рекомендации[В разработке] Раздел V. SDK и UIГлава 41. О содержании разделаГлава 42. SDK: проблемы и решенияГлава 43. КодогенерацияГлава 44. UI-компонентыГлава 45. Декомпозиция UI-компонентов. MV*-подходыГлава 46. MV*-фреймворкиГлава 47. Backend-Driven UIГлава 48. Разделяемые ресурсы и асинхронные блокировкиГлава 49. Вычисляемые свойстваГлава 50. В заключениеРаздел VI. API как продуктГлава 51. Продукт APIГлава 52. Бизнес-модели APIГлава 53. Формирование продуктового виденияГлава 54. Взаимодействие с разработчикамиГлава 55. Взаимодействие с бизнес-аудиториейГлава 56. Линейка сервисов APIГлава 57. Ключевые показатели эффективности APIГлава 58. Идентификация пользователей и борьба с фродомГлава 59. Технические способы борьбы с несанкционированным доступом к APIГлава 60. Поддержка пользователей APIГлава 61. ДокументацияГлава 62. Тестовая средаГлава 63. Управление ожиданиями @@ -598,7 +598,7 @@ ul.references li p a.back-anchor { Это произведение доступно по лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция — Некоммерческое использование») 4.0 Всемирная. Исходный код доступен на github.com/twirl/The-API-Book - Поделиться: facebook · twitter · linkedin · redditСодержаниеВведениеГлава 1. О структуре этой книгиГлава 2. Определение APIГлава 3. Обзор существующих решений в области разработки APIГлава 4. Критерии качества APIГлава 5. API-first подходГлава 6. Обратная совместимостьГлава 7. О версионированииГлава 8. Условные обозначения и терминологияРаздел I. Проектирование APIГлава 9. Пирамида контекстов APIГлава 10. Определение области примененияГлава 11. Разделение уровней абстракцииГлава 12. Разграничение областей ответственностиГлава 13. Описание конечных интерфейсовГлава 14. Приложение к разделу I. Модельный APIРаздел II. Паттерны дизайна APIГлава 15. О паттернах проектирования в контексте APIГлава 16. Аутентификация партнёров и авторизация вызовов APIГлава 17. Стратегии синхронизацииГлава 18. Слабая консистентностьГлава 19. Асинхронность и управление временемГлава 20. Списки и организация доступа к нимГлава 21. Двунаправленные потоки данных. Push и poll-моделиГлава 22. Мультиплексирование сообщений. Асинхронная обработка событийГлава 23. Атомарность массовых измененийГлава 24. Частичные обновленияГлава 25. Деградация и предсказуемостьРаздел III. Обратная совместимостьГлава 26. Постановка проблемы обратной совместимостиГлава 27. О ватерлинии айсбергаГлава 28. Расширение через абстрагированиеГлава 29. Сильная связность и сопутствующие проблемыГлава 30. Слабая связностьГлава 31. Интерфейсы как универсальный паттернГлава 32. Блокнот душевного покоя[В разработке] Раздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологииГлава 34. Мифология RESTГлава 35. Составляющие HTTP запросов и их семантикаГлава 36. Преимущества и недостатки HTTP APIГлава 37. Организация HTTP API согласно принципам RESTГлава 38. Разработка номенклатуры URL ресурсов и операций над нимиГлава 39. Работа с ошибками в HTTP APIГлава 40. Заключительные положения и общие рекомендации[В разработке] Раздел V. SDK и UIГлава 41. О содержании разделаГлава 42. SDK: проблемы и решенияГлава 43. КодогенерацияГлава 44. UI-компонентыГлава 45. Декомпозиция UI-компонентов. MV*-подходыГлава 46. MV*-фреймворкиГлава 47. Backend-Driven UIГлава 48. Разделяемые ресурсы и асинхронные блокировкиГлава 49. Вычисляемые свойстваГлава 50. В заключениеРаздел VI. API как продуктГлава 51. Продукт APIГлава 52. Бизнес-модели APIГлава 53. Формирование продуктового виденияГлава 54. Взаимодействие с разработчикамиГлава 55. Взаимодействие с бизнес-аудиториейГлава 56. Линейка сервисов APIГлава 57. Ключевые показатели эффективности APIГлава 58. Идентификация пользователей и борьба с фродомГлава 59. Технические способы борьбы с несанкционированным доступом к APIГлава 60. Поддержка пользователей APIГлава 61. ДокументацияГлава 62. Тестовая средаГлава 63. Управление ожиданиями + Поделиться: facebook · twitter · linkedin · redditСодержаниеВведениеГлава 1. О структуре этой книгиГлава 2. Определение APIГлава 3. Обзор существующих решений в области разработки APIГлава 4. Критерии качества APIГлава 5. API-first подходГлава 6. Обратная совместимостьГлава 7. О версионированииГлава 8. Условные обозначения и терминологияРаздел I. Проектирование APIГлава 9. Пирамида контекстов APIГлава 10. Определение области примененияГлава 11. Разделение уровней абстракцииГлава 12. Разграничение областей ответственностиГлава 13. Описание конечных интерфейсовГлава 14. Приложение к разделу I. Модельный APIРаздел II. Паттерны дизайна APIГлава 15. О паттернах проектирования в контексте APIГлава 16. Аутентификация партнёров и авторизация вызовов APIГлава 17. Стратегии синхронизацииГлава 18. Слабая консистентностьГлава 19. Асинхронность и управление временемГлава 20. Списки и организация доступа к нимГлава 21. Двунаправленные потоки данных. Push и poll-моделиГлава 22. Мультиплексирование сообщений. Асинхронная обработка событийГлава 23. Атомарность массовых измененийГлава 24. Частичные обновленияГлава 25. Деградация и предсказуемостьРаздел III. Обратная совместимостьГлава 26. Постановка проблемы обратной совместимостиГлава 27. О ватерлинии айсбергаГлава 28. Расширение через абстрагированиеГлава 29. Сильная связность и сопутствующие проблемыГлава 30. Слабая связностьГлава 31. Интерфейсы как универсальный паттернГлава 32. Блокнот душевного покояРаздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологииГлава 34. Мифология RESTГлава 35. Составляющие HTTP запросов и их семантикаГлава 36. Преимущества и недостатки HTTP APIГлава 37. Организация HTTP API согласно принципам RESTГлава 38. Разработка номенклатуры URL ресурсов. CRUD-операцииГлава 39. Работа с ошибками в HTTP APIГлава 40. Заключительные положения и общие рекомендации[В разработке] Раздел V. SDK и UIГлава 41. О содержании разделаГлава 42. SDK: проблемы и решенияГлава 43. КодогенерацияГлава 44. UI-компонентыГлава 45. Декомпозиция UI-компонентов. MV*-подходыГлава 46. MV*-фреймворкиГлава 47. Backend-Driven UIГлава 48. Разделяемые ресурсы и асинхронные блокировкиГлава 49. Вычисляемые свойстваГлава 50. В заключениеРаздел VI. API как продуктГлава 51. Продукт APIГлава 52. Бизнес-модели APIГлава 53. Формирование продуктового виденияГлава 54. Взаимодействие с разработчикамиГлава 55. Взаимодействие с бизнес-аудиториейГлава 56. Линейка сервисов APIГлава 57. Ключевые показатели эффективности APIГлава 58. Идентификация пользователей и борьба с фродомГлава 59. Технические способы борьбы с несанкционированным доступом к APIГлава 60. Поддержка пользователей APIГлава 61. ДокументацияГлава 62. Тестовая средаГлава 63. Управление ожиданиями § @@ -2227,6 +2227,7 @@ X-Idempotency-Token: <токен> Если бы автору этой книги давали доллар каждый раз, когда ему приходилось бы имплементировать кем-то придуманный дополнительный протокол безопасности — он бы давно уже был на заслуженной пенсии. Любовь разработчиков API к подписыванию параметров запросов или сложным схемам обмена паролей на токены столь же несомненна, сколько и бессмысленна. Во-первых, почти всегда процедуры, обеспечивающие безопасность той или иной операции, уже разработаны. Нет никакой нужды придумывать их заново, просто имплементируйте какой-то из существующих протоколов. Никакие самописные алгоритмы проверки сигнатур запросов не обеспечат вам того же уровня защиты от атаки Man-in-the-Middle, как соединение по протоколу TLS с взаимной проверкой сигнатур сертификатов. Во-вторых, чрезвычайно самонадеянно (и опасно) считать, что вы разбираетесь в вопросах безопасности. Новые вектора атаки появляются каждый день, и быть в курсе всех актуальных проблем — это само по себе работа на полный рабочий день. Если же вы полный рабочий день занимаетесь чем-то другим, спроектированная вами система защиты наверняка будет содержать уязвимости, о которых вы просто никогда не слышали — например, ваш алгоритм проверки паролей может быть подвержен атаке по времени, а веб-сервер — атаке с разделением запросов. +Фонд OWASP каждый год составляет список самых распространённых уязвимостей в API, который мы настоятельно рекомендуем изучить. Отдельно уточним: любые API должны предоставляться строго по протоколу TLS версии не ниже 1.2 (лучше 1.3). 25. Помогайте партнёрам не изобретать безопасность Не менее важно не только обеспечивать безопасность API как такового, но и предоставить партнёрам такие интерфейсы, которые минимизируют возможные проблемы с безопасностью на их стороне. @@ -2665,8 +2666,11 @@ const pendingOrders = await api. Учитывая, что клиентское приложение может быть перезапущено или просто потерять токен, наиболее правильное (хотя не всегда приемлемое с точки зрения нагрузки) поведение сервера при отсутствии токена в запросе — форсировать возврат актуальных мастер-данных. Риски перехода к событийной консистентности -Прежде всего, давайте зафиксируем один важный тезис: все обсуждаемые в настоящем разделе техники решения архитектурных проблем — вероятностные. Отказ от строгой консистентности означает, что даже при идеальной работе компонентов системы клиентские ошибки все равно будут возникать — мы только можем постараться сделать так, чтобы при типичном профиле использования системы ошибок было меньше. -Оговорка про «типичный профиль важна»: API предполагает вариативность сценариев его применения, и вполне может оказаться так, что кейсы использования API делятся на несколько сильно отличающихся с точки зрения толерантности к ошибкам групп (классический пример — это клиентские API, где завершения операций ждёт реальный пользователь, против серверных API, где время исполнения само по себе менее важно, но может оказаться важным, например, массовый параллелизм операций). Если такое происходит — это сильный сигнал для того, чтобы выделить API для различных типовых сценариев в отдельные продукты в семействе API, о чём мы поговорим в главе «Линейка сервисов API» раздела «API как продукт». +Прежде всего, давайте зафиксируем один важный тезис: все обсуждаемые в настоящем разделе техники решения архитектурных проблем — вероятностные. Отказ от строгой консистентности означает, что даже при идеальной работе компонентов системы клиентские ошибки все равно будут возникать. Может показаться, что этот фон ошибок можно просто проигнорировать, но это весьма рискованно. +Представим, что в нашей системе из-за событийной консистентности клиенты с какой-то вероятностью не могут сделать заказ с первой попытки. Например, пользователь добавляет в приложении новый метод оплаты, но при создании заказа попадает на реплику, которая ещё не получила данные о новом способе оплаты. Так как пользователи довольно часто совершают эти две операции (добавление банковской карты и заказ) подряд, фон ошибок будет довольно значительным — пусть для примера 1% — но нас это пока не беспокоит: в худшем случае клиент выполнит автоматический перезапрос. +Предположим теперь, однако, что в новой версии приложения была допущена ошибка, и 0.1% пользователей не могут выполнить заказ вовсе по причине того, что клиент отсылает неправильный идентификатор метода оплаты. В отсутствие 1% ошибок консистентности данных эта проблема была бы выявлена очень быстро; но на фоне имеющихся ошибок найти её весьма непросто: для этого требуется настроить мониторинги так, чтобы они точно исключали ошибки, вызванные нестрогой консистентностью данных, а это может быть весьма непросто, а то и вообще невозможно. Автор этой книги сталкивался с такими ситуациями в своей работе: ошибку, затрагивающую небольшой процент пользователей, можно не замечать месяцами. +Таким образом, задача проактивного снижения фона ошибок критически важна. Мы можем постараться сделать так, чтобы при типичном профиле использования системы ошибок было меньше. +NB: оговорка про «типичный профиль» здесь не просто так: API предполагает вариативность сценариев его применения, и вполне может оказаться так, что кейсы использования API делятся на несколько сильно отличающихся с точки зрения толерантности к ошибкам групп (классический пример — это клиентские API, где завершения операций ждёт реальный пользователь, против серверных API, где время исполнения само по себе менее важно, но может оказаться важным, например, массовый параллелизм операций). Если такое происходит — это сильный сигнал для того, чтобы выделить API для различных типовых сценариев в отдельные продукты в семействе API, о чём мы поговорим в главе «Линейка сервисов API» раздела «API как продукт». Проиллюстрируем этот принцип на нашем примере с заказом кофе. Предположим, что мы реализуем следующую схему: оптимистичное управление синхронизацией (скажем, через идентификатор последнего заказа); @@ -4482,7 +4486,7 @@ ProgramContext.dispatch = (action) => { NB. Идеальным примером строгого избегания данного антипаттерна следует признать разработку компиляторов — в этой сфере принято компилировать новую версию компилятора при помощи его же предыдущей версии. 5. Заведите блокнот Несмотря на все приёмы и принципы, изложенные в настоящем разделе, с большой вероятностью вы ничего не сможете сделать с накапливающейся неконсистентностью вашего API. Да, можно замедлить скорость накопления, предусмотреть какие-то проблемы заранее, заложить запасы устойчивости — но предугадать всё решительно невозможно. На этом этапе многие разработчики склонны принимать скоропалительные решения — т.е. выпускать новые минорные версии API с явным или неявным нарушением обратной совместимости в целях исправления ошибок дизайна. -Так делать мы крайне не рекомендуем — поскольку, напомним, API является помимо прочего и мультипликатором ваших ошибок. Что мы рекомендуем — так это завести блокнот душевного покоя, где вы будете записывать выученные уроки, которые потом нужно будет не забыть применить на практике при выпуске новой мажорной версии API.[В разработке] Раздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологии +Так делать мы крайне не рекомендуем — поскольку, напомним, API является помимо прочего и мультипликатором ваших ошибок. Что мы рекомендуем — так это завести блокнот душевного покоя, где вы будете записывать выученные уроки, которые потом нужно будет не забыть применить на практике при выпуске новой мажорной версии API.Раздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологии Вопросы организации HTTP API — к большому сожалению, одни из самых «холиварных». Будучи одной из самых популярных и притом весьма непростых в понимании технологий (ввиду большого по объёму и фрагментированного на отдельные RFC стандарта), спецификация HTTP обречена быть плохо понятой и превратно истолкованной миллионами разработчиков и многими тысячами учебных пособий. Поэтому, прежде чем переходить непосредственно к полезной части настоящего раздела, мы обязаны дать уточнения, о чём же всё-таки пойдёт речь. Так сложилось, что в настоящий момент сетевой стек, используемый для разработки клиент-серверных API, практически полностью унифицирован в двух точках. Одна из них — это Internet protocol suite, состоящий из базового протокола IP и надстройки в виде TCP или UDP над ним. На сегодняшний день альтернативы TCP/IP используются в чрезвычайно ограниченном спектре задач, и средний разработчик практически не сталкивается ни с каким другим сетевым стеком. Однако у TCP/IP с прикладной точки зрения есть существенный недостаток — он оперирует поверх системы IP-адресов, которые плохо подходят для организации распределённых систем: @@ -4490,7 +4494,7 @@ ProgramContext.dispatch = (action) => { во-вторых, IP-адрес является технической сущностью, связанной с узлом сети, а разработчики хотели бы иметь возможность добавлять и изменять узлы, не нарушая работы своих приложений. Удобной (и опять же имеющей почти стопроцентное проникновение) абстракцией над IP-адресами оказалась система доменных имён, позволяющий назначить узлам сети человекочитаемые синонимы. -Появление доменных имён потребовало разработки клиент-серверных протоколов более высокого, чем TCP/IP, уровня, и для передачи текстовых (гипертекстовых) данных таким протоколом стал HTTP 0.9, разработанный Тимом Бёрнерсом-Ли опубликованный в 1991 году. Помимо поддержки обращения к узлам сети по именам, HTTP также предоставил ещё одну очень удобную абстракцию, а именно назначение собственных адресов эндпойнтам, работающих на одном сетевом узле. +Появление доменных имён потребовало разработки клиент-серверных протоколов более высокого, чем TCP/IP, уровня, и для передачи текстовых (гипертекстовых) данных таким протоколом стал HTTP 0.9, разработанный Тимом Бёрнерсом-Ли опубликованный в 1991 году. Помимо поддержки обращения к узлам сети по именам, HTTP также предоставил ещё одну очень удобную абстракцию, а именно назначение собственных адресов эндпойнтам, работающим на одном сетевом узле. Протокол был очень прост и всего лишь описывал способ получить документ, открыв TCP/IP соединение с сервером и передав строку вида GET адрес_документа. Позднее протокол был дополнен стандартом URL, позволяющим детализировать адрес документа, и далее протокол начал развиваться стремительно: появились новые глаголы помимо GET, статусы ответов, заголовки, типы данных и так далее. HTTP появился изначально для передачи размеченного гипертекста, что для программных интерфейсов подходит слабо. Однако HTML быстро эволюционировал в более строгий и машиночитаемый XML, который быстро стал одним из общепринятых форматов описания вызовов API. С начала 2000-х XML начал вытесняться более простым и интероперабельным JSON, и сегодня говоря о HTTP API, чаще всего имеют в виду такие интерфейсы, в которых данные передаются в формате JSON по протоколу HTTP. Поскольку, с одной стороны, HTTP был простым и понятным протоколом, позволяющим осуществлять произвольные запросы к удаленным серверам по их доменным именам, и, с другой стороны, быстро оброс почти бесконечным количеством разнообразных расширений над базовой функциональностью, он довольно быстро стал второй точкой, к которой сходятся сетевые технологии: практически все запросы к API внутри TCP/IP-сетей осуществляются по протоколу HTTP (и даже если используется альтернативный протокол, запросы в нём всё равно зачастую оформлены в виде HTTP-пакетов просто ради удобства). При этом, однако, в отличие от TCP/IP-уровня, каждый разработчик сам для себя решает, какой объём функциональности, предоставляемой протоколом и многочисленными расширениями к нему, он готов применить. В частности, gRPC и GraphQL работают поверх HTTP, но используют крайне ограниченное подмножество его возможностей. @@ -4774,7 +4778,7 @@ X-ApiName-Partner-Id: <partner_id> } } -сервер мог бы ответить просто 400 Bad Request (с передачей идентификатора запроса, ну скажем, в заголовке X-OurCoffeeAPI-Request-Id). Тем не менее, разработчики протокола посчитали нужным разработать свой собственный формат. +сервер мог бы ответить просто 400 Bad Request (с передачей идентификатора запроса, ну скажем, в заголовке X-OurCoffeeAPI-RequestId). Тем не менее, разработчики протокола посчитали нужным разработать свой собственный формат. Такая ситуация (не только конкретно с JSON-RPC, а почти со всеми высокоуровневыми протоколами поверх HTTP) сложилась по множеству причин, включая разнообразные исторические (например, невозможность использовать многие возможности HTTP из ранних реализаций XMLHttpRequest в браузерах). Однако, новые варианты RPC-протоколов, использующих абсолютный минимум возможностей HTTP, продолжают появляться и сегодня. Чтобы разрешить этот парадокс, обратим внимание на одно принципиальное различие между использованием протоколов уровня приложения (как в нашем примере с JSON-RPC) и чистого HTTP: если ошибка 400 Bad Request является прозрачной для практически любого сетевого агента, то ошибка в собственном формате JSON-RPC таковой не является — во-первых, потому что понять её может только агент с поддержкой JSON-RPC, а, во-вторых, что более важно, в JSON-RPC статус запроса не является метаинформацией. Протокол HTTP позволяет прочитать такие детали, как метод и URL запроса, статус операции, заголовки запроса и ответа, не читая тело запроса целиком. Для большинства протоколов более высокого уровня, включая JSON-RPC, это не так: даже если агент и обладает поддержкой протокола, ему необходимо прочитать и разобрать тело ответа. Каким образом эта самая возможность читать метаданные нам может быть полезна? Современный стек взаимодействия между клиентом и сервером является (как и предсказывал Филдинг) многослойным. Мы можем выделить множество агентов разного уровня, которые, так или иначе, обрабатывают сетевые запросы и ответы: @@ -4795,7 +4799,7 @@ X-ApiName-Partner-Id: <partner_id> ПО для разработчиков, позволяющее удобным образом разрабатывать и отлаживать клиенты API (Postman, Insomnia) и так далее. Конечно, большинство этих инструментов применимы и для работы с API, реализующими альтернативные парадигмы. Однако именно способность промежуточных агентов считывать метаданные HTTP запросов позволяет легко строить сложные конвейеры типа экспортировать access-логи nginx в Prometheus и из коробки получить удобные мониторинги статус-кодов ответов в Grafana. -Отдельно заметим что HTTP API является на сегодняшний день выбором по умолчанию при разработке публичных API. В силу озвученных выше причин, как бы ни был устроен технологический стек партнёра, интегрироваться с HTTP API он сможет без особых усилий. При этом распространённость технологии понижает и порог входа, и требования к квалификации инженеров партёра. +Отдельно заметим что HTTP API является на сегодняшний день выбором по умолчанию при разработке публичных API. В силу озвученных выше причин, как бы ни был устроен технологический стек партнёра, интегрироваться с HTTP API он сможет без особых усилий. При этом распространённость технологии понижает и порог входа, и требования к квалификации инженеров партнёра. Главным недостатком HTTP API является то, что промежуточные агенты, от клиентских фреймворков до API-гейтвеев, умеют читать метаданные запроса и выполнять какие-то действия с их использованием — настраивать политику перезапросов и таймауты, логировать, кэшировать, шардировать, проксировать и так далее — даже если вы их об этом не просили. Более того, так как стандарты HTTP являются сложными, концепция REST — непонятной, а разработчики программного обеспечения — неидеальными, то промежуточные агенты (и разработчики партнёра!) могут трактовать метаданные запроса неправильно. Особенно это касается каких-то экзотических и сложных в имплементации стандартов. Как правило, одной из причин разработки новых RPC-фреймворков декларируется стремление обеспечить простоту и консистентность работы с протоколом, чтобы таким образом уменьшить поле для потенциальных ошибок в реализации интеграции с API. Указанное выше соображение распространяется не только на программное обеспечение, но и на его создателей. Представление разработчиков о HTTP API, увы, также фрагментировано. Практически любой программист как-то умеет работать с HTTP API, но редко при этом знает стандарт или хотя бы консультируется с ним при написании кода. Это ведёт к тому, что добиться качественной и консистентной реализации логики работы с HTTP API может быть сложнее, нежели при использовании альтернативных технологий — причём это соображение справедливо как для партнёров-интеграторов, так и для самого провайдера API. Вопросы производительности @@ -4895,7 +4899,7 @@ HTTP/1.1 200 OK сервисы B и C обратятся к сервису A, проверят токен (переданный через проксирование заголовка Authorization или как явный параметр запроса), и вернут данные по запросу — профиль пользователя и список его заказов; сервис D скомбинирует ответы сервисов B и C и вернёт их клиенту. -Исходная схема организации микросервисов. Нажмите для увеличения +Исходная схема организации микросервисов. Нажмите для увеличения Нетрудно заметить, что мы тем самым создаём излишнюю нагрузку на сервис A: теперь к нему обращается каждый из вложенных микросервисов; даже если мы откажемся от аутентификации пользователей в конечных сервисах, оставив её только в сервисе D, проблему это не решит, поскольку сервисы B и C самостоятельно выяснить идентификатор пользователя не могут. Очевидный способ избавиться от лишних запросов — сделать так, чтобы однажды полученный user_id передавался остальным сервисам по цепочке: гейтвей D получает запрос и через сервис A меняет токен на user_id @@ -4907,7 +4911,7 @@ HTTP/1.1 200 OK -Шаг 1. Явные идентификаторы пользователей. Нажмите для увеличения +Шаг 1. Явные идентификаторы пользователей. Нажмите для увеличения NB: мы использовали нотацию /v1/orders?user_id, а не, допустим, /v1/users/{user_id}/orders по двум причинам: @@ -4961,7 +4965,7 @@ If-None-Match: <ревизия> -Шаг 2. Добавление серверного кэширования. Нажмите для увеличения +Шаг 2. Добавление серверного кэширования. Нажмите для увеличения Использовав такое решение [функциональность управления кэшом через ETag ресурсов], мы автоматически получаем ещё один приятный бонус: эти же данные пригодятся нам, если пользователь попытается создать новый заказ. Если мы используем оптимистичное управление параллелизмом, то клиент должен передать в запросе актуальную ревизию ресурса orders: POST /v1/orders HTTP/1.1 If-Match: <ревизия> @@ -4978,7 +4982,7 @@ ETag: <новая ревизия> { /* обновлённый список текущих заказов */ } и обновить кэш в соответствии с новыми данными. -Создание нового заказа. Нажмите для увеличения +Создание нового заказа. Нажмите для увеличения Важно: обратите внимание на то, что, после всех преобразований, мы получили систему, в которой мы можем убрать гейтвей D и возложить его функции непосредственно на клиентский код. В самом деле, ничто не мешает клиенту: хранить на своей стороне user_id (либо извлекать его из токена, если формат позволяет) и последний полученный ETag состояния списка заказов; @@ -5043,7 +5047,7 @@ Authorization: Bearer <token> Этот подход можно в дальнейшем усложнять: добавлять гранулярные разрешения выполнять конкретные операции, вводить уровни доступа, проверку прав в реальном времени через дополнительный вызов ACL-сервиса и так далее. -Важно, что кажущаяся избыточность перестала быть таковой: user_id в запросе теперь не дублируется в данных токена; эти идентификаторы имеют разный смысл: над каким ресурсом исполняется операция и кто исполняет операцию. Совпадение этих двух сущностей — пусть частотный, но всё же частный случай. Что, к сожалению, не отменяет его неочевидности и возможности легко забыть выполнить проверку в коде. Таков путь.Глава 38. Разработка номенклатуры URL ресурсов и операций над ними +Важно, что кажущаяся избыточность перестала быть таковой: user_id в запросе теперь не дублируется в данных токена; эти идентификаторы имеют разный смысл: над каким ресурсом исполняется операция и кто исполняет операцию. Совпадение этих двух сущностей — пусть частотный, но всё же частный случай. Что, к сожалению, не отменяет его неочевидности и возможности легко забыть выполнить проверку в коде. Таков путь.Глава 38. Разработка номенклатуры URL ресурсов. CRUD-операции Как мы уже отмечали в предыдущих главах, стандарты HTTP и URL, а также принципы REST, не предписывают определённой семантики значимым компонентам URL (в частности, частям path и парам ключ-значение в query). Правила организации URL в HTTP API существуют только для читабельности кода и удобства разработчика. Что, впрочем, совершенно не означает, что они неважны: напротив, URL в HTTP API являются средством выразить уровни абстракции и области ответственности объектов. Правильный дизайн иерархии сущностей в API должен быть отражён в правильном дизайне номенклатуры URL. NB: отсутствие строгих правил естественным образом привело к тому, что многие разработчики их просто придумали сами для себя. Некоторые наиболее распространённые стихийные практики, например, требование использовать в URL только существительные, в советах по разработке HTTP API в Интернете часто выдаются за стандарты или требования REST, которыми они не являются. Тем не менее, демонстративное игнорирование таких самопровозглашённых правил тоже не лучший подход для провайдера API, поскольку он увеличивает шансы быть неверно понятым. Традиционно частям URL приписывается следующая семантика: @@ -5054,6 +5058,22 @@ Authorization: Bearer <token> Подобная конвенция достаточно хорошо подходит для того, чтобы отразить номенклатуру сущностей почти любого API, поэтому следовать ей вполне разумно (и, наоборот, демонстративное нарушение этого устоявшегося соглашения чревато тем, что разработчики вас просто неправильно поймут). Однако подобная некодифицированная и размытая концепция неизбежно вызывает множество разночтений в конкретных моментах: +Где заканчиваются метаданные операции и начинаются «просто» данные и насколько допустимо дублировать поля и там, и там? Например, одна из распространённых практик в HTTP API — индицировать тип возвращаемых данных путём добавки «расширения» к URL, подобно именам файлов в файловых системах (т.е. при обращении к ресурсу /v1/orders/{id}.xml ответ будет получен в формате XML, а при обращении к /v1/orders/{id}.json — в формате JSON). С одной стороны, для определения формата данных предназначены Accept*-заголовки. С другой стороны, читабельность кода явно повышается с введением параметра «формат» прямо в URL. + + +Как следствие предыдущего пункта — каким образом в HTTP API правильно указывать версию самого API? Навскидку можно предложить как минимум три варианта, каждый из которых вполне соответствует букве стандарта: + +как path-параметр: /v1/orders/{id}; +как query-параметр: /orders/{id}?version=1; +как заголовок: +GET /orders/{id} HTTP/1.1 +X-OurCoffeeAPI-Version: 1 + + + +Сюда можно приплюсовать и более экзотические варианты, такие как указание схемы в кастомизированном медиатипе или протоколе запроса. + + Каким образом организовывать эндпойнты, связывающие две сущности, между которыми нет явных отношений подчинения? Скажем, каким должен быть URL запуска приготовления лунго на конкретной кофе-машине? /coffee-machines/{id}/recipes/lungo/prepare @@ -5078,17 +5098,18 @@ Authorization: Bearer <token> NB: эта дихотомия волнует не только нас, но и авторов стандарта, которые в конечном итоге предложили новый глагол QUERY, который по сути является немодифицирующим POST. Мы, однако, сомневаемся, что он получит широкое распространение — поскольку уже существующий SEARCH оказался в этом качестве никому не нужен. -Простых ответов на вопросы выше у нас, к сожалению, нет. В рамках настоящей книги мы придерживаемся следующего подхода: +Простых ответов на вопросы выше у нас, к сожалению, нет. В рамках настоящей книги мы придерживаемся следующего подхода: сигнатура вызова в первую очередь должна быть лаконична и читабельна. Усложнение сигнатур в угоду абстрактным концепциям нежелательно. Применительно к указанным проблемам это означает, что: + +Метаданные операции не должны менять смысл операции; если запрос доходит до конечного микросервиса вообще без заголовков, он всё ещё должен быть выполним, хотя какая-то вспомогательная функциональность может деградировать или отсутствовать. +Мы используем указание версии в path по одной простой причине: все остальные способы сделать это имеют смысл если и только если при изменении мажорной версии протокола номенклатура URL останется прежней. Но, если номенклатура ресурсов может быть сохранена, то нет никакой нужды нарушать обратную совместимость нет. +Иерархия ресурсов выдерживается там, где она однозначна (т.е., если сущность низшего уровня абстракции однозначно подчинена сущности высшего уровня абстракции, то отношения между ними будут выражены в виде вложенных путей). -сигнатура вызова в первую очередь должна быть лаконична и читабельна; усложнение сигнатур в угоду абстрактным концепциям нежелательно; -иерархия ресурсов выдерживается там, где она однозначна (т.е., если сущность низшего уровня абстракции однозначно подчинена сущности высшего уровня абстракции, то отношения между ними будут выражены в виде вложенных путей); - -если есть сомнения в том, что иерархия в ходе дальнейшего развития API останется неизменной, лучше завести новый верхнеуровневый префикс, а не вкладывать новые сущности в уже существующие; +Если есть сомнения в том, что иерархия в ходе дальнейшего развития API останется неизменной, лучше завести новый верхнеуровневый префикс, а не вкладывать новые сущности в уже существующие. -для выполнения «кросс-доменных» операций (т.е. при необходимости сослаться на объекты разных уровней абстракции в одном вызове) предпочтительнее завести специальный ресурс, выполняющий операцию (т.е. в примере с кофе-машинами и рецептами автор этой книги выбрал бы вариант /prepare?coffee_machine_id=<id>&recipe=lungo); -семантика HTTP-глагола приоритетнее ложного предупреждения о небезопасности/неидемпотентности (в частности, если операция является безопасной, но ресурсозатратной, с нашей точки зрения вполне разумно использовать метод POST для индикации этого факта). - +Для выполнения «кросс-доменных» операций (т.е. при необходимости сослаться на объекты разных уровней абстракции в одном вызове) предпочтительнее завести специальный ресурс, выполняющий операцию (т.е. в примере с кофе-машинами и рецептами автор этой книги выбрал бы вариант /prepare?coffee_machine_id=<id>&recipe=lungo). +Семантика HTTP-вызова приоритетнее ложного предупреждения о небезопасности/неидемпотентности (в частности, если операция является безопасной, но ресурсозатратной, с нашей точки зрения вполне разумно использовать метод POST для индикации этого факта). + NB: отметим, что передача параметров в виде пути или query-параметра в URL влияет не только на читабельность. Вернёмся к примеру из предыдущей главы и представим, что гейтвей D реализован в виде stateless прокси с декларативной конфигурацией. Тогда получать от клиента запрос в виде: @@ -5121,7 +5142,7 @@ Authorization: Bearer <token> С точки зрения удобства разработки концепция соответствия CRUD и HTTP выглядит очень удобной — каждому виду ресурсов соответствует свой URL, каждой операции — свой глагол. При пристальном рассмотрении, однако, оказывается, что это отношение — очень упрощённое представление о манипуляции ресурсами, и, что самое неприятное, плохо расширяемое. 1. Создание -Начнём с операции создания ресурса. Как мы помним из главы «Стратегии синхронизации”, операция создания в любой сколько-нибудь ответственной предметной области обязана быть идемпотентной и, очень желательно, ещё и позволять управлять параллелизмом. В рамках парадигмы HTTP API идемпотентное создание можно организовать одним из трёх способов: +Начнём с операции создания ресурса. Как мы помним из главы «Стратегии синхронизации», операция создания в любой сколько-нибудь ответственной предметной области обязана быть идемпотентной и, очень желательно, ещё и позволять управлять параллелизмом. В рамках парадигмы HTTP API идемпотентное создание можно организовать одним из трёх способов: Через метод POST с передачей токена идемпотентности (им может выступать, в частности, ETag ресурса): @@ -5190,7 +5211,7 @@ Location: /v1/orders/{id} GET /v1/orders/{order_id}/attachements/{id} 3. Редактирование -Проблемы частичного обновления ресурсов мы подробно разбирали в соответствующей главе раздела «Паттерны дизайна API». Напомним, что полная перезапись ресурса методом PUT возможна, но быстро разбивается о необходимость работать с вычисляемыми и неизменяемыми полями, необходимость совместного редактирования и/или большой объём передаваемых данных. Работа через метод PATCH возможна, но, так как этот метод по умолчанию считается неидемпотентным (и часто нетразитивным), для него справедливо всё то же соображение об опасности автоматических перезапросов. Достаточно быстро мы придём к одному из двух вариантов: +Проблемы частичного обновления ресурсов мы подробно разбирали в соответствующей главе раздела «Паттерны дизайна API». Напомним, что полная перезапись ресурса методом PUT возможна, но быстро разбивается о необходимость работать с вычисляемыми и неизменяемыми полями, необходимость совместного редактирования и/или большой объём передаваемых данных. Работа через метод PATCH возможна, но, так как этот метод по умолчанию считается неидемпотентным (и часто нетранзитивным), для него справедливо всё то же соображение об опасности автоматических перезапросов. Достаточно быстро мы придём к одному из двух вариантов: либо PUT декомпозирован на множество составных PUT /v1/orders/{id}/address, PUT /v1/orders/{id}/volume и т.д. — по ресурсу для каждой частной операции; либо существует отдельный ресурс, принимающий список изменений, причём, вероятнее всего, через схему черновик-подтверждение в виде пары методов POST + PUT. @@ -5200,7 +5221,227 @@ Location: /v1/orders/{id} С удалением ситуация проще всего: никакие данные в современных сервисах не удаляются моментально, а лишь архивируются или помечаются удалёнными. Таким образом, вместо DELETE /v1/orders/{id} необходимо разработать эндпойнт типа PUT /v1/orders/{id}/archive или PUT /v1/archive?order=<order_id>. В качестве заключения Идея CRUD как способ минимальным набором операций описать типовые действия над ресурсом в при столкновении с реальностью быстро эволюционирует в сторону семейства эндпойнтов, каждый из которых описывает отдельный аспект взаимодействия с сущностью в течение её жизненного цикла. -Изложенные выше соображения следует считать не критикой концепции CRUD как таковой, а скорее призывом не лениться и разрабатывать номенклатуру ресурсов и операций над ними исходя из конкретной предметной области, а не абстрактных мнемонических правил, к которым является эта концепция. Если вы всё же хотите разработать типовой API для манипуляции типовыми сущностями, стоит изначально разработать его гораздо более гибким, чем предлагает CRUD-HTTP методология.Глава 39. Работа с ошибками в HTTP APIГлава 40. Заключительные положения и общие рекомендации[В разработке] Раздел V. SDK и UIГлава 41. О содержании разделаГлава 42. SDK: проблемы и решенияГлава 43. КодогенерацияГлава 44. UI-компонентыГлава 45. Декомпозиция UI-компонентов. MV*-подходыГлава 46. MV*-фреймворкиГлава 47. Backend-Driven UIГлава 48. Разделяемые ресурсы и асинхронные блокировкиГлава 49. Вычисляемые свойстваГлава 50. В заключениеРаздел VI. API как продуктГлава 51. Продукт API +Изложенные выше соображения следует считать не критикой концепции CRUD как таковой, а скорее призывом не лениться и разрабатывать номенклатуру ресурсов и операций над ними исходя из конкретной предметной области, а не абстрактных мнемонических правил, к которым является эта концепция. Если вы всё же хотите разработать типовой API для манипуляции типовыми сущностями, стоит изначально разработать его гораздо более гибким, чем предлагает CRUD-HTTP методология.Глава 39. Работа с ошибками в HTTP API +Рассмотренные в предыдущих главах примеры организации API согласно стандарту HTTP и принципам REST покрывают т.н. «happy path», т.е. стандартный процесс работы с API в отсутствие ошибок. Конечно, нам не менее интересен и обратный кейс — каким образом HTTP API следует работать с ошибками, и чем стандарт и архитектурные принципы могут нам в этом помочь. Пусть какой-то агент в системе (неважно, клиент или гейтвей) пытается создать новый заказ: +POST /v1/orders?user_id=<user_id> HTTP/1.1 +Authorization: Bearer <token> +If-Match: <ревизия> + +{ /* параметры заказа */ } + +Какие потенциальные неприятности могут ожидать нас при выполнении этого запроса? Навскидку, это: + +Запрос не может быть прочитан (недопустимые символы, нарушение синтаксиса). +Токен авторизации отсутствует. +Токен авторизации невалиден. +Токен валиден, но пользователь не обладает правами создавать новый заказ. +Пользователь удалён или деактивирован. +Идентификатор пользователя неверен (не существует). +Ревизия не передана. +Ревизия не совпадает с последней актуальной. +В теле запроса отсутствуют обязательные поля. +Какое-то из полей запроса имеет недопустимое значение. +Превышены лимиты на допустимое количество запросов. +Сервер перегружен и не может ответить в настоящий момент. +Неизвестная серверная ошибка (т.е. сервер сломан настолько, что диагностика ошибки невозможна). + +Исходя из общих соображений, соблазнительной кажется идея назначить каждой из ошибок свой статус-код. Скажем, для ошибки (4) напрашивается код 403, а для ошибки (11) — 429. Не будем, однако, торопиться, и прежде зададим себе вопрос с какой целью мы хотим назначить тот или иной код ошибки. +В нашей системе в общем случае присутствуют три агента: пользователь приложения, само приложение (клиент) и сервер. Каждому из этих акторов необходимо понимать ответ на три вопроса относительно ошибки (причём для каждого из акторов ответ может быть разным): + +Кто допустил ошибку (конечный пользователь, разработчик клиента, разработчик сервера или какой-то промежуточный агент, например, программист сетевого стека). + +Не забудем учесть тот факт, что и конечный пользователь, и разработчик клиента могут допустить ошибку намеренно, например, пытаясь перебором подобрать пароль к чужому аккаунту. + + +Можно ли исправить ошибку, просто повторив запрос. + +Если да, то через какое время. + + +Если повтором запроса ошибку исправить нельзя, то можно ли её исправить, переформулировав запрос. +Если ошибку вообще нельзя исправить, то что с этим делать. + +На один из этих вопрос в рамках стандарта HTTP ответить достаточно легко: регулировать желаемое время повтора запроса можно через параметры кэширования ответа и заголовок Retry-After. Также HTTP частично помогает с первым вопросом: для определения, на чьей стороне произошла ошибка, используется первая цифра статус-кода (см. ниже). +Со всеми остальными вопросами, увы, ситуация сильно сложнее. +Клиентские ошибки +Статус-коды, начинающиеся с цифры 4, индицируют, что ошибка допущена пользователем или клиентом (или, по крайней мере, сервер так считает). Обычно, полученную 4xx повторять бессмысленно — если не предпринять дополнительных действий по изменению состояния сервиса, этот запрос не будет выполнен успешно никогда. Однако из этого правила есть исключения, самые важные из которых — 429 Too Many Requests и 404 Not Found. Последняя по стандарту имеет смысл «состояния неопределённости»: сервер имеет право использовать её, если не желает раскрывать причины ошибки. После получения ошибки 404, можно сделать повторный запрос, и он вполне может отработать успешно. Для индикации персистентной ошибки «ресурс не найден» используется отдельный статус 410 Gone. +Более интересный вопрос — а что всё-таки клиент может (или должен) сделать, получив такую ошибку. Как мы указывали в главе «Разграничение областей ответственности», если ошибка может быть исправлена программно, необходимо в машиночитаемом виде индицировать это клиенту; если ошибка не может быть исправлена, необходимо включить человекочитаемые сообщения для пользователя (даже просто «попробуйте начать сначала / перезагрузить приложение» лучше с точки зрения UX, чем «неизвестная ошибка») и для разработчика, который будет разбираться с проблемой. +С восстановимыми ошибками в HTTP, к сожалению, ситуация достаточно сложная. С одной стороны, протокол включает в себя множество специальных кодов, которые индицируют проблемы с использованием самого протокола — такие как 405 Method Not Allowed (данный глагол неприменим к указанному ресурсу), 406 Not Acceptable (сервер не может вернуть ответ согласно Accept-заголовкам запроса), 411 Length Required, 414 URI Too Long и так далее. Код клиента может обработать данные ошибки и даже, возможно, предпринять какие-то действия по их устранению (например, добавить заголовок Content-Length в запрос после получения ошибки 411), но все они очень плохо применимы к ошибкам в бизнес-логике. Например, мы можем вернуть 429 Too Many Requests при превышении лимитов запросов, но у нас нет никакого стандартного способа указать, какой именно лимит был превышен. +Частично проблему отсутствия стандартных подходов к возврату ошибок компенсируют использованием различных близких по смыслу статус-кодов для индикации разных состояний (либо и вовсе выбор произвольного кода ошибки и придания ему нового смысла в рамках конкретного API). В частности, сегодня де-факто стандартом является возврат кода 401 Unauthorized при отсутствии заголовков авторизации или невалидном токене (получение этого кода, таким образом, является сигналом для приложения предложить пользователю залогиниться в системе), что противоречит стандарту (который требует при возврате 401 обязательно указать заголовок WWW-Authenticate с описанием способа аутентификации пользователя; нам неизвестны реальные API, которые выполняют это требованием). +Однако таких кодов, которые могут отражать нюансы одной и той же проблемы, в стандарте очень мало. Фактически, мы приходим к тому, что множество различных ошибок в логике приложения приходится возвращать под очень небольшим набором статус-кодов: + +400 Bad Request для всех ошибок валидации запроса (некоторые пуристы утверждают, что, вообще говоря, 400 соответствует нарушению формата запроса — невалидному JSON, например — а для логических ошибок следует использовать код 422 Unprocessable Content; в постановке задачи это мало что меняет); +403 Forbidden для любых проблем, связанных с авторизацией действий клиента; +404 Not Found в случае, если какие-то из указанных в запросе сущностей не найдены либо раскрытие причин ошибки нежелательно; +409 Conflict при нарушении целостности данных; +410 Gone если ресурс был удалён; +429 Too Many Requests при превышении лимитов. + +Разработчики стандарта HTTP об этой проблеме вполне осведомлены, и отдельно отмечают, что для решения бизнес-сценариев необходимо передавать в метаданных либо теле ответа дополнительные данные для описания возникшей ситуации («the server SHOULD send a representation containing an explanation of the error situation, and whether it is a temporary or permanent condition»), что (как и введение новых специальных кодов ошибок) противоречит самой идее унифицированного машиночитаемого формата ошибок. (Отметим, что отсутствие стандартов описания ошибок в бизнес-логике — одна из основных причин, по которым мы считаем разработку REST API как его описал Филдинг в манифесте 2008 года невозможной; клиент должен обладать априорным знанием о том, как работать с метаинформацией об ошибке, иначе он сможет восстанавливать своё состояние после ошибки только перезагрузкой.) +Дополнительно, у проблемы есть и третье измерение в виде серверного ПО мониторинга состояния системы, которое часто полагается на статус-коды ответов при построении графиков и уведомлений. Между тем, ошибки, скрывающиеся под одним статус кодом — например ввод неправильного пароля и истёкший срок жизни токена — могут быть очень разными по смыслу; повышенный фон первой ошибки может говорить о потенциальной попытке взлома путём перебора паролей, а второй — о потенциальных ошибках в новой версии приложения, которая может неверно кэшировать токены авторизации. +Всё это естественным образом подводит нас к следующему выводу: если мы хотим использовать ошибки для диагностики и (возможно) восстановления состояния клиента, нам необходимо добавить машиночитаемую метаинформацию о подвиде ошибки и, возможно, тело ошибки с указанием подробной информации о проблемах — например, как мы предлагали в главе «Описание конечных интерфейсов»: +POST /v1/coffee-machines/search HTTP/1.1 + +{ + "recipes": ["lngo"], + "position": { + "latitude": 110, + "longitude": 55 + } +} +→ +HTTP/1.1 400 Bad Request +X-OurCoffeeAPI-Error-Kind:⮠ + wrong_parameter_value + +{ + "reason": "wrong_parameter_value", + "localized_message": + "Что-то пошло не так.⮠ + Обратитесь к разработчику приложения." + "details": { + "checks_failed": [ + { + "field": "recipe", + "error_type": "wrong_value", + "message": + "Value 'lngo' unknown.⮠ + Did you mean 'lungo'?" + }, + { + "field": "position.latitude", + "error_type": "constraint_violation", + "constraints": { + "min": -90, + "max": 90 + }, + "message": + "'position.latitude' value⮠ + must fall within⮠ + the [-90, 90] interval" + } + ] + } +} + +Также напомним, что любые неизвестные 4xx-статус-коды клиент должен трактовать как ошибку 400 Bad Request, следовательно, формат (мета)данных ошибки 400 должен быть максимально общим. +Серверные ошибки +Ошибки 5xx индицируют, что клиент, со своей стороны, выполнил запрос правильно, и проблема заключается в сервере. Для клиента, по большому счёту, важно только то, имеет ли смысл повторять запрос и, если да, то через какое время. Если учесть, что в любых публично доступных API причины серверных ошибок, как правило, не раскрывают — в абсолютном большинстве кодов 500 Internal Server Error и 503 Service Unavailable достаточно для индикации серверных ошибок (второй код указывает, что отказ в обслуживании имеет разовый характер и есть смысл автоматически повторить запрос), или можно вовсе ограничиться одним из них с опциональным заголовком Retry-After. +Для внутренних систем, вообще говоря, такое рассуждение неверно. Для построения правильных мониторингов и системы оповещений необходимо, чтобы серверные ошибки, точно так же, как и клиентские, содержали подтип ошибки в машиночитаемом виде. Здесь по-прежнему применимы те же подходы — использование широкой номенклатуры кодов и/или передача типа ошибки заголовком — однако эта информация должна быть вырезана гейтвеем на границе внешней и внутренней систем, и заменена на общую информацию для разработчика и для конечного пользователя системы с описанием действий, которые необходимо выполнить при получении ошибки. +POST /v1/orders/?user_id=<user id> HTTP/1.1 +If-Match: <ревизия> + +{ parameters } +→ +// Ответ, полученный гейтвеем +// от сервиса обработки заказов, +// метаданные которого будут +// использованы для мониторинга +HTTP/1.1 500 Internal Server Error +// Тип ошибки: получен таймаут от БД +X-OurCoffeAPI-Error-Kind: db_timeout +{ /* + * Дополнительные данные, например, + * какой хост ответил таймаутом + */ } + +// Ответ, передаваемый клиенту. +// Детали серверной ошибки удалены +// и заменены на инструкцию клиенту. +// Поскольку гейтвей не знает, был +// ли в действительности сделан заказ, +// клиенту рекомендуется попробовать +// повторить запрос и/или попытаться +// получить актуальное состояние +HTTP/1.1 500 Internal Server Error +Retry-After: 5 + +{ + "reason": "internal_server_error", + "localized_message": "Не удалось⮠ + получить ответ от сервера.⮠ + Попробуйте повторить операцию + или обновить страницу.", + "details": { + "can_be_retried": true, + "is_operation_failed": "unknown" + } +} + +Вот здесь мы, однако, вступаем на очень скользкую территорию. Современная практика реализации HTTP-клиентов такова, что безусловно повторяются только немодифицирующие (GET, HEAD, OPTIONS) запросы. В случае модифицирующих запросов разработчик должен написать код, который повторит запрос — и для этого разработчику нужно очень внимательно прочитать документацию к API, чтобы убедиться, что это поведение допустимо и не приведёт к побочным эффектам. +Теоретически идемпотентные методы PUT и DELETE можно вызывать повторно. Практически, однако, ввиду того, что многие разработчики упускают требование идемпотентности этих методов, фреймворки работы с HTTP API по умолчанию перезапросов модифицирующих методов, как правило, не делают, но некоторую выгоду из следования стандарту мы всё же можем извлечь — по крайней мере, сама сигнатура индицирует, что запрос можно повторять. +Что касается более сложных ситуаций, когда мы хотим указать разработчику, что он может безопасно повторить потенциально неидемпотентную операцию, то мы могли бы предложить формат описания доступных действий в теле ошибки… но практически никто не ожидает найти такое описание в самой ошибке. Возможно, потому, что с ошибками 5xx, в отличие от 4xx, программисты практически не сталкиваются при написании клиентского кода, и мало какие тестовые среды позволяют такие ошибки эмулировать. Так или иначе, описывать необходимые действия при получении серверной ошибки вам придётся в документации. (Имейте в виду, что эти инструкции с большой долей вероятности будут проигнорированы. Таков путь.) +Организация системы ошибок в HTTP API на практике +Как понятно из вышесказанного, фактически есть три способа работать с ошибками HTTP API: + + +Расширительно трактовать номенклатуру статус-кодов и использовать новый код каждый раз, когда требуется индицировать новый вид ошибки. (Автор этой книги неоднократно встречал ситуации, когда при разработке API просто выбирался «похоже выглядящий» статус безо всякой оглядки на его описание в стандарте.) + + +Полностью отказаться от использования статус-кодов и вкладывать описание ошибки в тело и/или метаданные ответа с кодом 200. Этим путём идут почти все RPC-фреймворки. + +2а. Вариантом этой стратегии можно считать использование всего двух статус-кодов ошибок (400 для любой клиентской ошибки, 500 для любой серверной), опционально трёх (те же плюс 404 для статуса неопределённости). + + + +Применить смешанный подход, то есть использовать статус-код согласно его семантике для индикации рода ошибки и вложенные (мета)данные в специально разработанном формате для детализации (подобно фрагментам кода, предложенным нами в настоящей главе). + + +Как нетрудно заметить, считать соответствующим стандарту можно только подход (3). Будем честны и скажем, что выгоды следования ему, особенно по сравнению с вариантом (2а), не очень велики и состоят в основном в чуть лучшей читабельности логов и большей прозрачности для промежуточных прокси.Глава 40. Заключительные положения и общие рекомендации +Подведём итог описанному в предыдущих главах. Чтобы разработать качественный HTTP API, необходимо: + +Описать happy path, т.е. составить диаграмму вызовов для стандартного цикла работы клиентского приложения. +Определить каждый вызов как операцию над некоторым ресурсом и, таким образом, составить номенклатуру URL и применимых методов. +Понять, какие ошибки возможны при выполнении операций и каким образом клиент должен восстанавливаться из какого состояния. +Решить, какая функциональность будет передана на уровень протокола HTTP [какие стандартные возможности протокола будут использованы в сопряжении с какими инструментами разработки] и в каком объёме. +Опираясь на решения 1-4, разработать конкретную спецификацию. +Проверить себя: пройти по пунктам 1-3, написать псевдокод бизнес-логики приложения согласно разработанной спецификации, и оценить, насколько удобным, понятным и читабельным оказался результирующий API. + +Позволим себе так же дать несколько советов по code style: + + +Не различайте пути с / на конце и без него и примите какую-то рекомендацию по умолчанию (мы рекомендуем все пути заканчивать на / — по простой причине, это позволяет разумно описать обращение к корню домена как ГЛАГОЛ /). Если вы решили запретить один из вариантов (скажем, пути без слэша в конце), при обращении по второму варианту должен быть или редирект или однозначно читаемая ошибка. + + +Включайте в ответы стандартные заголовки — Date, Content-Type, Content-Encoding, Content-Length, Cache-Control, Retry-After — и вообще старайтесь не полагаться на то, что клиент правильно догадывается о параметрах протокола по умолчанию. + + +Поддержите метод OPTIONS и протокол CORS на случай, если ваш API захотят использовать из браузеров. + + +Определитесь с правилами выбора кейсинга параметров (и преобразований кейсинга при перемещении параметра между различными частями запроса) и придерживайтесь их. + + +Всегда оставляйте себе возможность обратно-совместимого расширения операции API. В частности, всегда возвращайте корневой JSON-объект в ответах эндпойтов — потому что приписать новые поля к объекту вы можете, а к массивам и примитивам — нет. + +Отметим также, что пустая строка не является валидным JSON, поэтому корректнее возвращать пустой объект {} там, где ответа не подразумевается (или статус 204 No Content с пустым телом, но тогда эндпойнт нельзя будет расширить в будущем). + + + +Для всех GET-запросов указывайте политику кэширования (иначе всегда есть шанс, что клиент или промежуточный агент придумает её за вас). + + +Не эксплуатируйте известные возможности оперировать запросами в нарушение стандарта и не изобретайте свои решения для «серых зон» протокола. В частности: + +не размещайте модифицирующие операции за методом GET и неидемпотентные операции за PUT / DELETE; +соблюдайте симметрию GET / PUT / DELETE методов; +не позволяйте GET / HEAD / DELETE-запросам иметь тело, не возвращайте тело в ответе метода HEAD или совместно со статус-кодом 204 No Content; +не придумывайте свой стандарт для передачи массивов и вложенных объектов в query — лучше воспользоваться HTTP-глаголом, позволяющим запросу иметь тело, или, в крайнем случае, передать параметры в виде base64-кодированного JSON-поля; +не размещайте в пути и домене URL параметры, по формату требующие эскейпинга (т.е. могущие содержать символы, отличные от цифр и букв латинского алфавита); для этой цели лучше воспользоваться query-параметрами или телом запроса. + + + +Ознакомьтесь хотя бы с основными видами уязвимостей в типичных имплементациях HTTP API, которыми могут воспользоваться злоумышленники: + +CSFR +SSRF +HTTP Response Splitting +Unvalidated Redirects and Forwards + +и заложите защиту от этих векторов атак на уровне вашего серверного ПО. Организация OWASP предоставляет хороший обзор лучших security-практик для HTTP API. + + +В заключение хотелось бы сказать следующее: HTTP API — это способ организовать ваше API так, чтобы полагаться на понимание семантики операций как разнообразным программным обеспечением, от клиентских фреймворков до серверных гейтвеев, так и разработчиком, который читает спецификацию. В этом смысле экосистема HTTP предоставляет пожалуй что наиболее широкий (и в плане глубины, и в плане распространённости) по сравнению с другими технологиями словарь для описания самых разнообразных ситуаций, возникающих во время работы клиент-серверных приложений. Разумеется, эта технология не лишена своих недостатков, но для разработчика публичного API она является выбором по умолчанию — на сегодняшний день скорее надо обосновывать отказ от HTTP API чем выбор в его пользу.[В разработке] Раздел V. SDK и UIГлава 41. О содержании разделаГлава 42. SDK: проблемы и решенияГлава 43. КодогенерацияГлава 44. UI-компонентыГлава 45. Декомпозиция UI-компонентов. MV*-подходыГлава 46. MV*-фреймворкиГлава 47. Backend-Driven UIГлава 48. Разделяемые ресурсы и асинхронные блокировкиГлава 49. Вычисляемые свойстваГлава 50. В заключениеРаздел VI. API как продуктГлава 51. Продукт API Когда мы говорим об API как о продукте, необходимо чётко зафиксировать два важных тезиса. diff --git a/docs/API.ru.pdf b/docs/API.ru.pdf index f2d8c81..a75d685 100644 Binary files a/docs/API.ru.pdf and b/docs/API.ru.pdf differ diff --git a/docs/index.html b/docs/index.html index 07663ce..6205ccf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -107,16 +107,16 @@ - [Work in Progress] Section IV. HTTP APIs & REST Architectural Principles + Section IV. HTTP APIs & REST Architectural Principles Chapter 33. On the HTTP API Concept and Terminology Chapter 34. The REST Myth Chapter 35. Components of an HTTP Request and Their Semantics Chapter 36. Advantages and Disadvantages of HTTP APIs Chapter 37. Organizing HTTP APIs Based on the REST Principles -Chapter 38. Designing a Nomenclature of URLs and Applicable Operations -Chapter 39. Working with HTTP API Errors -Chapter 40. Final Provisions and General Recommendations +Chapter 38. Designing a Nomenclature of URLs. CRUD Operations +Chapter 39. Working with HTTP API Errors +Chapter 40. Final Provisions and General Recommendations diff --git a/docs/index.ru.html b/docs/index.ru.html index 3ffdbdf..a336eac 100644 --- a/docs/index.ru.html +++ b/docs/index.ru.html @@ -107,16 +107,16 @@ - [В разработке] Раздел IV. HTTP API и REST + Раздел IV. HTTP API и REST Глава 33. О концепции HTTP API и терминологии Глава 34. Мифология REST Глава 35. Составляющие HTTP запросов и их семантика Глава 36. Преимущества и недостатки HTTP API Глава 37. Организация HTTP API согласно принципам REST -Глава 38. Разработка номенклатуры URL ресурсов и операций над ними -Глава 39. Работа с ошибками в HTTP API -Глава 40. Заключительные положения и общие рекомендации +Глава 38. Разработка номенклатуры URL ресурсов. CRUD-операции +Глава 39. Работа с ошибками в HTTP API +Глава 40. Заключительные положения и общие рекомендации diff --git a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/01.md b/src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/01.md similarity index 100% rename from src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/01.md rename to src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/01.md diff --git a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/02.md b/src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/02.md similarity index 100% rename from src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/02.md rename to src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/02.md diff --git a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/03.md b/src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/03.md similarity index 100% rename from src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/03.md rename to src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/03.md diff --git a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/04.md b/src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/04.md similarity index 100% rename from src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/04.md rename to src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/04.md diff --git a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/05.md b/src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/05.md similarity index 100% rename from src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/05.md rename to src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/05.md diff --git a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/06.md b/src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/06.md similarity index 100% rename from src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/06.md rename to src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/06.md diff --git a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/07.md b/src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/07.md similarity index 100% rename from src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/07.md rename to src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/07.md diff --git a/src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/08.md b/src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/08.md similarity index 100% rename from src/en/clean-copy/05-[Work in Progress] Section IV. HTTP APIs & REST Architectural Principles/08.md rename to src/en/clean-copy/05-Section IV. HTTP APIs & REST Architectural Principles/08.md diff --git a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/01.md b/src/ru/clean-copy/05-Раздел IV. HTTP API и REST/01.md similarity index 100% rename from src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/01.md rename to src/ru/clean-copy/05-Раздел IV. HTTP API и REST/01.md diff --git a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/02.md b/src/ru/clean-copy/05-Раздел IV. HTTP API и REST/02.md similarity index 100% rename from src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/02.md rename to src/ru/clean-copy/05-Раздел IV. HTTP API и REST/02.md diff --git a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/03.md b/src/ru/clean-copy/05-Раздел IV. HTTP API и REST/03.md similarity index 100% rename from src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/03.md rename to src/ru/clean-copy/05-Раздел IV. HTTP API и REST/03.md diff --git a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/04.md b/src/ru/clean-copy/05-Раздел IV. HTTP API и REST/04.md similarity index 100% rename from src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/04.md rename to src/ru/clean-copy/05-Раздел IV. HTTP API и REST/04.md diff --git a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/05.md b/src/ru/clean-copy/05-Раздел IV. HTTP API и REST/05.md similarity index 100% rename from src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/05.md rename to src/ru/clean-copy/05-Раздел IV. HTTP API и REST/05.md diff --git a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/06.md b/src/ru/clean-copy/05-Раздел IV. HTTP API и REST/06.md similarity index 100% rename from src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/06.md rename to src/ru/clean-copy/05-Раздел IV. HTTP API и REST/06.md diff --git a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/07.md b/src/ru/clean-copy/05-Раздел IV. HTTP API и REST/07.md similarity index 100% rename from src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/07.md rename to src/ru/clean-copy/05-Раздел IV. HTTP API и REST/07.md diff --git a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/08.md b/src/ru/clean-copy/05-Раздел IV. HTTP API и REST/08.md similarity index 100% rename from src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/08.md rename to src/ru/clean-copy/05-Раздел IV. HTTP API и REST/08.md
@@ -598,7 +598,7 @@ ul.references li p a.back-anchor { Это произведение доступно по лицензии Creative Commons «Attribution-NonCommercial» («Атрибуция — Некоммерческое использование») 4.0 Всемирная. Исходный код доступен на github.com/twirl/The-API-Book - Поделиться: facebook · twitter · linkedin · redditСодержаниеВведениеГлава 1. О структуре этой книгиГлава 2. Определение APIГлава 3. Обзор существующих решений в области разработки APIГлава 4. Критерии качества APIГлава 5. API-first подходГлава 6. Обратная совместимостьГлава 7. О версионированииГлава 8. Условные обозначения и терминологияРаздел I. Проектирование APIГлава 9. Пирамида контекстов APIГлава 10. Определение области примененияГлава 11. Разделение уровней абстракцииГлава 12. Разграничение областей ответственностиГлава 13. Описание конечных интерфейсовГлава 14. Приложение к разделу I. Модельный APIРаздел II. Паттерны дизайна APIГлава 15. О паттернах проектирования в контексте APIГлава 16. Аутентификация партнёров и авторизация вызовов APIГлава 17. Стратегии синхронизацииГлава 18. Слабая консистентностьГлава 19. Асинхронность и управление временемГлава 20. Списки и организация доступа к нимГлава 21. Двунаправленные потоки данных. Push и poll-моделиГлава 22. Мультиплексирование сообщений. Асинхронная обработка событийГлава 23. Атомарность массовых измененийГлава 24. Частичные обновленияГлава 25. Деградация и предсказуемостьРаздел III. Обратная совместимостьГлава 26. Постановка проблемы обратной совместимостиГлава 27. О ватерлинии айсбергаГлава 28. Расширение через абстрагированиеГлава 29. Сильная связность и сопутствующие проблемыГлава 30. Слабая связностьГлава 31. Интерфейсы как универсальный паттернГлава 32. Блокнот душевного покоя[В разработке] Раздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологииГлава 34. Мифология RESTГлава 35. Составляющие HTTP запросов и их семантикаГлава 36. Преимущества и недостатки HTTP APIГлава 37. Организация HTTP API согласно принципам RESTГлава 38. Разработка номенклатуры URL ресурсов и операций над нимиГлава 39. Работа с ошибками в HTTP APIГлава 40. Заключительные положения и общие рекомендации[В разработке] Раздел V. SDK и UIГлава 41. О содержании разделаГлава 42. SDK: проблемы и решенияГлава 43. КодогенерацияГлава 44. UI-компонентыГлава 45. Декомпозиция UI-компонентов. MV*-подходыГлава 46. MV*-фреймворкиГлава 47. Backend-Driven UIГлава 48. Разделяемые ресурсы и асинхронные блокировкиГлава 49. Вычисляемые свойстваГлава 50. В заключениеРаздел VI. API как продуктГлава 51. Продукт APIГлава 52. Бизнес-модели APIГлава 53. Формирование продуктового виденияГлава 54. Взаимодействие с разработчикамиГлава 55. Взаимодействие с бизнес-аудиториейГлава 56. Линейка сервисов APIГлава 57. Ключевые показатели эффективности APIГлава 58. Идентификация пользователей и борьба с фродомГлава 59. Технические способы борьбы с несанкционированным доступом к APIГлава 60. Поддержка пользователей APIГлава 61. ДокументацияГлава 62. Тестовая средаГлава 63. Управление ожиданиями + Поделиться: facebook · twitter · linkedin · redditСодержаниеВведениеГлава 1. О структуре этой книгиГлава 2. Определение APIГлава 3. Обзор существующих решений в области разработки APIГлава 4. Критерии качества APIГлава 5. API-first подходГлава 6. Обратная совместимостьГлава 7. О версионированииГлава 8. Условные обозначения и терминологияРаздел I. Проектирование APIГлава 9. Пирамида контекстов APIГлава 10. Определение области примененияГлава 11. Разделение уровней абстракцииГлава 12. Разграничение областей ответственностиГлава 13. Описание конечных интерфейсовГлава 14. Приложение к разделу I. Модельный APIРаздел II. Паттерны дизайна APIГлава 15. О паттернах проектирования в контексте APIГлава 16. Аутентификация партнёров и авторизация вызовов APIГлава 17. Стратегии синхронизацииГлава 18. Слабая консистентностьГлава 19. Асинхронность и управление временемГлава 20. Списки и организация доступа к нимГлава 21. Двунаправленные потоки данных. Push и poll-моделиГлава 22. Мультиплексирование сообщений. Асинхронная обработка событийГлава 23. Атомарность массовых измененийГлава 24. Частичные обновленияГлава 25. Деградация и предсказуемостьРаздел III. Обратная совместимостьГлава 26. Постановка проблемы обратной совместимостиГлава 27. О ватерлинии айсбергаГлава 28. Расширение через абстрагированиеГлава 29. Сильная связность и сопутствующие проблемыГлава 30. Слабая связностьГлава 31. Интерфейсы как универсальный паттернГлава 32. Блокнот душевного покояРаздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологииГлава 34. Мифология RESTГлава 35. Составляющие HTTP запросов и их семантикаГлава 36. Преимущества и недостатки HTTP APIГлава 37. Организация HTTP API согласно принципам RESTГлава 38. Разработка номенклатуры URL ресурсов. CRUD-операцииГлава 39. Работа с ошибками в HTTP APIГлава 40. Заключительные положения и общие рекомендации[В разработке] Раздел V. SDK и UIГлава 41. О содержании разделаГлава 42. SDK: проблемы и решенияГлава 43. КодогенерацияГлава 44. UI-компонентыГлава 45. Декомпозиция UI-компонентов. MV*-подходыГлава 46. MV*-фреймворкиГлава 47. Backend-Driven UIГлава 48. Разделяемые ресурсы и асинхронные блокировкиГлава 49. Вычисляемые свойстваГлава 50. В заключениеРаздел VI. API как продуктГлава 51. Продукт APIГлава 52. Бизнес-модели APIГлава 53. Формирование продуктового виденияГлава 54. Взаимодействие с разработчикамиГлава 55. Взаимодействие с бизнес-аудиториейГлава 56. Линейка сервисов APIГлава 57. Ключевые показатели эффективности APIГлава 58. Идентификация пользователей и борьба с фродомГлава 59. Технические способы борьбы с несанкционированным доступом к APIГлава 60. Поддержка пользователей APIГлава 61. ДокументацияГлава 62. Тестовая средаГлава 63. Управление ожиданиями § @@ -2227,6 +2227,7 @@ X-Idempotency-Token: <токен> Если бы автору этой книги давали доллар каждый раз, когда ему приходилось бы имплементировать кем-то придуманный дополнительный протокол безопасности — он бы давно уже был на заслуженной пенсии. Любовь разработчиков API к подписыванию параметров запросов или сложным схемам обмена паролей на токены столь же несомненна, сколько и бессмысленна. Во-первых, почти всегда процедуры, обеспечивающие безопасность той или иной операции, уже разработаны. Нет никакой нужды придумывать их заново, просто имплементируйте какой-то из существующих протоколов. Никакие самописные алгоритмы проверки сигнатур запросов не обеспечат вам того же уровня защиты от атаки Man-in-the-Middle, как соединение по протоколу TLS с взаимной проверкой сигнатур сертификатов. Во-вторых, чрезвычайно самонадеянно (и опасно) считать, что вы разбираетесь в вопросах безопасности. Новые вектора атаки появляются каждый день, и быть в курсе всех актуальных проблем — это само по себе работа на полный рабочий день. Если же вы полный рабочий день занимаетесь чем-то другим, спроектированная вами система защиты наверняка будет содержать уязвимости, о которых вы просто никогда не слышали — например, ваш алгоритм проверки паролей может быть подвержен атаке по времени, а веб-сервер — атаке с разделением запросов. +Фонд OWASP каждый год составляет список самых распространённых уязвимостей в API, который мы настоятельно рекомендуем изучить. Отдельно уточним: любые API должны предоставляться строго по протоколу TLS версии не ниже 1.2 (лучше 1.3). 25. Помогайте партнёрам не изобретать безопасность Не менее важно не только обеспечивать безопасность API как такового, но и предоставить партнёрам такие интерфейсы, которые минимизируют возможные проблемы с безопасностью на их стороне. @@ -2665,8 +2666,11 @@ const pendingOrders = await api. Учитывая, что клиентское приложение может быть перезапущено или просто потерять токен, наиболее правильное (хотя не всегда приемлемое с точки зрения нагрузки) поведение сервера при отсутствии токена в запросе — форсировать возврат актуальных мастер-данных. Риски перехода к событийной консистентности -Прежде всего, давайте зафиксируем один важный тезис: все обсуждаемые в настоящем разделе техники решения архитектурных проблем — вероятностные. Отказ от строгой консистентности означает, что даже при идеальной работе компонентов системы клиентские ошибки все равно будут возникать — мы только можем постараться сделать так, чтобы при типичном профиле использования системы ошибок было меньше. -Оговорка про «типичный профиль важна»: API предполагает вариативность сценариев его применения, и вполне может оказаться так, что кейсы использования API делятся на несколько сильно отличающихся с точки зрения толерантности к ошибкам групп (классический пример — это клиентские API, где завершения операций ждёт реальный пользователь, против серверных API, где время исполнения само по себе менее важно, но может оказаться важным, например, массовый параллелизм операций). Если такое происходит — это сильный сигнал для того, чтобы выделить API для различных типовых сценариев в отдельные продукты в семействе API, о чём мы поговорим в главе «Линейка сервисов API» раздела «API как продукт». +Прежде всего, давайте зафиксируем один важный тезис: все обсуждаемые в настоящем разделе техники решения архитектурных проблем — вероятностные. Отказ от строгой консистентности означает, что даже при идеальной работе компонентов системы клиентские ошибки все равно будут возникать. Может показаться, что этот фон ошибок можно просто проигнорировать, но это весьма рискованно. +Представим, что в нашей системе из-за событийной консистентности клиенты с какой-то вероятностью не могут сделать заказ с первой попытки. Например, пользователь добавляет в приложении новый метод оплаты, но при создании заказа попадает на реплику, которая ещё не получила данные о новом способе оплаты. Так как пользователи довольно часто совершают эти две операции (добавление банковской карты и заказ) подряд, фон ошибок будет довольно значительным — пусть для примера 1% — но нас это пока не беспокоит: в худшем случае клиент выполнит автоматический перезапрос. +Предположим теперь, однако, что в новой версии приложения была допущена ошибка, и 0.1% пользователей не могут выполнить заказ вовсе по причине того, что клиент отсылает неправильный идентификатор метода оплаты. В отсутствие 1% ошибок консистентности данных эта проблема была бы выявлена очень быстро; но на фоне имеющихся ошибок найти её весьма непросто: для этого требуется настроить мониторинги так, чтобы они точно исключали ошибки, вызванные нестрогой консистентностью данных, а это может быть весьма непросто, а то и вообще невозможно. Автор этой книги сталкивался с такими ситуациями в своей работе: ошибку, затрагивающую небольшой процент пользователей, можно не замечать месяцами. +Таким образом, задача проактивного снижения фона ошибок критически важна. Мы можем постараться сделать так, чтобы при типичном профиле использования системы ошибок было меньше. +NB: оговорка про «типичный профиль» здесь не просто так: API предполагает вариативность сценариев его применения, и вполне может оказаться так, что кейсы использования API делятся на несколько сильно отличающихся с точки зрения толерантности к ошибкам групп (классический пример — это клиентские API, где завершения операций ждёт реальный пользователь, против серверных API, где время исполнения само по себе менее важно, но может оказаться важным, например, массовый параллелизм операций). Если такое происходит — это сильный сигнал для того, чтобы выделить API для различных типовых сценариев в отдельные продукты в семействе API, о чём мы поговорим в главе «Линейка сервисов API» раздела «API как продукт». Проиллюстрируем этот принцип на нашем примере с заказом кофе. Предположим, что мы реализуем следующую схему: оптимистичное управление синхронизацией (скажем, через идентификатор последнего заказа); @@ -4482,7 +4486,7 @@ ProgramContext.dispatch = (action) => { NB. Идеальным примером строгого избегания данного антипаттерна следует признать разработку компиляторов — в этой сфере принято компилировать новую версию компилятора при помощи его же предыдущей версии. 5. Заведите блокнот Несмотря на все приёмы и принципы, изложенные в настоящем разделе, с большой вероятностью вы ничего не сможете сделать с накапливающейся неконсистентностью вашего API. Да, можно замедлить скорость накопления, предусмотреть какие-то проблемы заранее, заложить запасы устойчивости — но предугадать всё решительно невозможно. На этом этапе многие разработчики склонны принимать скоропалительные решения — т.е. выпускать новые минорные версии API с явным или неявным нарушением обратной совместимости в целях исправления ошибок дизайна. -Так делать мы крайне не рекомендуем — поскольку, напомним, API является помимо прочего и мультипликатором ваших ошибок. Что мы рекомендуем — так это завести блокнот душевного покоя, где вы будете записывать выученные уроки, которые потом нужно будет не забыть применить на практике при выпуске новой мажорной версии API.[В разработке] Раздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологии +Так делать мы крайне не рекомендуем — поскольку, напомним, API является помимо прочего и мультипликатором ваших ошибок. Что мы рекомендуем — так это завести блокнот душевного покоя, где вы будете записывать выученные уроки, которые потом нужно будет не забыть применить на практике при выпуске новой мажорной версии API.Раздел IV. HTTP API и RESTГлава 33. О концепции HTTP API и терминологии Вопросы организации HTTP API — к большому сожалению, одни из самых «холиварных». Будучи одной из самых популярных и притом весьма непростых в понимании технологий (ввиду большого по объёму и фрагментированного на отдельные RFC стандарта), спецификация HTTP обречена быть плохо понятой и превратно истолкованной миллионами разработчиков и многими тысячами учебных пособий. Поэтому, прежде чем переходить непосредственно к полезной части настоящего раздела, мы обязаны дать уточнения, о чём же всё-таки пойдёт речь. Так сложилось, что в настоящий момент сетевой стек, используемый для разработки клиент-серверных API, практически полностью унифицирован в двух точках. Одна из них — это Internet protocol suite, состоящий из базового протокола IP и надстройки в виде TCP или UDP над ним. На сегодняшний день альтернативы TCP/IP используются в чрезвычайно ограниченном спектре задач, и средний разработчик практически не сталкивается ни с каким другим сетевым стеком. Однако у TCP/IP с прикладной точки зрения есть существенный недостаток — он оперирует поверх системы IP-адресов, которые плохо подходят для организации распределённых систем: @@ -4490,7 +4494,7 @@ ProgramContext.dispatch = (action) => { во-вторых, IP-адрес является технической сущностью, связанной с узлом сети, а разработчики хотели бы иметь возможность добавлять и изменять узлы, не нарушая работы своих приложений. Удобной (и опять же имеющей почти стопроцентное проникновение) абстракцией над IP-адресами оказалась система доменных имён, позволяющий назначить узлам сети человекочитаемые синонимы. -Появление доменных имён потребовало разработки клиент-серверных протоколов более высокого, чем TCP/IP, уровня, и для передачи текстовых (гипертекстовых) данных таким протоколом стал HTTP 0.9, разработанный Тимом Бёрнерсом-Ли опубликованный в 1991 году. Помимо поддержки обращения к узлам сети по именам, HTTP также предоставил ещё одну очень удобную абстракцию, а именно назначение собственных адресов эндпойнтам, работающих на одном сетевом узле. +Появление доменных имён потребовало разработки клиент-серверных протоколов более высокого, чем TCP/IP, уровня, и для передачи текстовых (гипертекстовых) данных таким протоколом стал HTTP 0.9, разработанный Тимом Бёрнерсом-Ли опубликованный в 1991 году. Помимо поддержки обращения к узлам сети по именам, HTTP также предоставил ещё одну очень удобную абстракцию, а именно назначение собственных адресов эндпойнтам, работающим на одном сетевом узле. Протокол был очень прост и всего лишь описывал способ получить документ, открыв TCP/IP соединение с сервером и передав строку вида GET адрес_документа. Позднее протокол был дополнен стандартом URL, позволяющим детализировать адрес документа, и далее протокол начал развиваться стремительно: появились новые глаголы помимо GET, статусы ответов, заголовки, типы данных и так далее. HTTP появился изначально для передачи размеченного гипертекста, что для программных интерфейсов подходит слабо. Однако HTML быстро эволюционировал в более строгий и машиночитаемый XML, который быстро стал одним из общепринятых форматов описания вызовов API. С начала 2000-х XML начал вытесняться более простым и интероперабельным JSON, и сегодня говоря о HTTP API, чаще всего имеют в виду такие интерфейсы, в которых данные передаются в формате JSON по протоколу HTTP. Поскольку, с одной стороны, HTTP был простым и понятным протоколом, позволяющим осуществлять произвольные запросы к удаленным серверам по их доменным именам, и, с другой стороны, быстро оброс почти бесконечным количеством разнообразных расширений над базовой функциональностью, он довольно быстро стал второй точкой, к которой сходятся сетевые технологии: практически все запросы к API внутри TCP/IP-сетей осуществляются по протоколу HTTP (и даже если используется альтернативный протокол, запросы в нём всё равно зачастую оформлены в виде HTTP-пакетов просто ради удобства). При этом, однако, в отличие от TCP/IP-уровня, каждый разработчик сам для себя решает, какой объём функциональности, предоставляемой протоколом и многочисленными расширениями к нему, он готов применить. В частности, gRPC и GraphQL работают поверх HTTP, но используют крайне ограниченное подмножество его возможностей. @@ -4774,7 +4778,7 @@ X-ApiName-Partner-Id: <partner_id> } } -сервер мог бы ответить просто 400 Bad Request (с передачей идентификатора запроса, ну скажем, в заголовке X-OurCoffeeAPI-Request-Id). Тем не менее, разработчики протокола посчитали нужным разработать свой собственный формат. +сервер мог бы ответить просто 400 Bad Request (с передачей идентификатора запроса, ну скажем, в заголовке X-OurCoffeeAPI-RequestId). Тем не менее, разработчики протокола посчитали нужным разработать свой собственный формат. Такая ситуация (не только конкретно с JSON-RPC, а почти со всеми высокоуровневыми протоколами поверх HTTP) сложилась по множеству причин, включая разнообразные исторические (например, невозможность использовать многие возможности HTTP из ранних реализаций XMLHttpRequest в браузерах). Однако, новые варианты RPC-протоколов, использующих абсолютный минимум возможностей HTTP, продолжают появляться и сегодня. Чтобы разрешить этот парадокс, обратим внимание на одно принципиальное различие между использованием протоколов уровня приложения (как в нашем примере с JSON-RPC) и чистого HTTP: если ошибка 400 Bad Request является прозрачной для практически любого сетевого агента, то ошибка в собственном формате JSON-RPC таковой не является — во-первых, потому что понять её может только агент с поддержкой JSON-RPC, а, во-вторых, что более важно, в JSON-RPC статус запроса не является метаинформацией. Протокол HTTP позволяет прочитать такие детали, как метод и URL запроса, статус операции, заголовки запроса и ответа, не читая тело запроса целиком. Для большинства протоколов более высокого уровня, включая JSON-RPC, это не так: даже если агент и обладает поддержкой протокола, ему необходимо прочитать и разобрать тело ответа. Каким образом эта самая возможность читать метаданные нам может быть полезна? Современный стек взаимодействия между клиентом и сервером является (как и предсказывал Филдинг) многослойным. Мы можем выделить множество агентов разного уровня, которые, так или иначе, обрабатывают сетевые запросы и ответы: @@ -4795,7 +4799,7 @@ X-ApiName-Partner-Id: <partner_id> ПО для разработчиков, позволяющее удобным образом разрабатывать и отлаживать клиенты API (Postman, Insomnia) и так далее. Конечно, большинство этих инструментов применимы и для работы с API, реализующими альтернативные парадигмы. Однако именно способность промежуточных агентов считывать метаданные HTTP запросов позволяет легко строить сложные конвейеры типа экспортировать access-логи nginx в Prometheus и из коробки получить удобные мониторинги статус-кодов ответов в Grafana. -Отдельно заметим что HTTP API является на сегодняшний день выбором по умолчанию при разработке публичных API. В силу озвученных выше причин, как бы ни был устроен технологический стек партнёра, интегрироваться с HTTP API он сможет без особых усилий. При этом распространённость технологии понижает и порог входа, и требования к квалификации инженеров партёра. +Отдельно заметим что HTTP API является на сегодняшний день выбором по умолчанию при разработке публичных API. В силу озвученных выше причин, как бы ни был устроен технологический стек партнёра, интегрироваться с HTTP API он сможет без особых усилий. При этом распространённость технологии понижает и порог входа, и требования к квалификации инженеров партнёра. Главным недостатком HTTP API является то, что промежуточные агенты, от клиентских фреймворков до API-гейтвеев, умеют читать метаданные запроса и выполнять какие-то действия с их использованием — настраивать политику перезапросов и таймауты, логировать, кэшировать, шардировать, проксировать и так далее — даже если вы их об этом не просили. Более того, так как стандарты HTTP являются сложными, концепция REST — непонятной, а разработчики программного обеспечения — неидеальными, то промежуточные агенты (и разработчики партнёра!) могут трактовать метаданные запроса неправильно. Особенно это касается каких-то экзотических и сложных в имплементации стандартов. Как правило, одной из причин разработки новых RPC-фреймворков декларируется стремление обеспечить простоту и консистентность работы с протоколом, чтобы таким образом уменьшить поле для потенциальных ошибок в реализации интеграции с API. Указанное выше соображение распространяется не только на программное обеспечение, но и на его создателей. Представление разработчиков о HTTP API, увы, также фрагментировано. Практически любой программист как-то умеет работать с HTTP API, но редко при этом знает стандарт или хотя бы консультируется с ним при написании кода. Это ведёт к тому, что добиться качественной и консистентной реализации логики работы с HTTP API может быть сложнее, нежели при использовании альтернативных технологий — причём это соображение справедливо как для партнёров-интеграторов, так и для самого провайдера API. Вопросы производительности @@ -4895,7 +4899,7 @@ HTTP/1.1 200 OK сервисы B и C обратятся к сервису A, проверят токен (переданный через проксирование заголовка Authorization или как явный параметр запроса), и вернут данные по запросу — профиль пользователя и список его заказов; сервис D скомбинирует ответы сервисов B и C и вернёт их клиенту.