As a complementary, for those who might meet the same issue as mine, I’m using $.ajax
to post form data to server and I also got the 400
error at first.
Assume I have a javascript variable,
var formData = {
"name":"Gearon",
"hobby":"Be different"
};
Do not use variable formData
directly as the value of key data
like below:
$.ajax({
type: "post",
dataType: "json",
url: "http://localhost/user/add",
contentType: "application/json",
data: formData,
success: function(data, textStatus){
alert("Data: " + data + "\nStatus: " + status);
}
});
Instead, use JSON.stringify to encapsulate the formData
as below:
$.ajax({
type: "post",
dataType: "json",
url: "http://localhost/user/add",
contentType: "application/json",
data: JSON.stringify(formData),
success: function(data, textStatus){
alert("Data: " + data + "\nStatus: " + status);
}
});
Anyway, as others have illustrated, the error is because the server could not recognize the request cause malformed syntax, I’m just raising a instance at practice. Hope it would be helpful to someone.
The following document provides reference information about the status codes
and error messages that are used in the Cloud Storage JSON API. For
the page specific to the Cloud Storage XML API, see
HTTP status and error codes for XML.
Error Response Format
Cloud Storage uses the standard HTTP error reporting format for the
JSON API. Successful requests return HTTP status codes in the 2xx range. Failed
requests return status codes in the 4xx and 5xx ranges. Requests that require a
redirect returns status codes in the 3xx range. Error responses usually include
a JSON document in the response body, which contains information about the
error.
The following examples show some common errors. Note that the header
information in the responses is omitted.
The following is an example of an error response you receive if you try to
list the buckets for a project but do not provide an authorization header.
401 Unauthorized { "error": { "errors": [ { "domain": "global", "reason": "required", "message": "Login Required", "locationType": "header", "location": "Authorization" } ], "code": 401, "message": "Login Required" } }
403 Forbidden
This is an example of an error response you receive if you try to list the
buckets of a non-existent project or one in which you don’t have permission
to list buckets.
403 Forbidden { "error": { "errors": [ { "domain": "global", "reason": "forbidden", "message": "Forbidden" } ], "code": 403, "message": "Forbidden" } }
404 Not Found
The following is an example of an error response you receive if you try to
retrieve an object that does not exist.
404 Not Found { "error": { "errors": [ { "domain": "global", "reason": "notFound", "message": "Not Found" } ], "code": 404, "message": "Not Found" } }
409 Conflict
The following is an example of an error response you receive if you try to
create a bucket using the name of a bucket you already own.
409 Conflict { "error": { "errors": [ { "domain": "global", "reason": "conflict", "message": "You already own this bucket. Please select another name." } ], "code": 409, "message": "You already own this bucket. Please select another name." } }
The following table describes the elements that can appear in the response body
of an error. Fields should be used together to help determine the problem.
Also, the example values given below are meant for illustration and are not an
exhaustive list of all possible values.
Element | Description |
---|---|
code |
An HTTP status code value, without the textual description.
Example values include: |
error |
A container for the error information. |
errors |
A container for the error details. |
errors.domain |
The scope of the error. Example values include: global and push . |
errors.location |
The specific item within the locationType that caused the error. For example, if you specify an invalid value for a parameter, the location will be the name of the parameter.
Example values include: |
errors.locationType |
The location or part of the request that caused the error. Use with location to pinpoint the error. For example, if you specify an invalid value for a parameter, the locationType will be parameter and the location will be the name of the parameter.
Example values include |
errors.message |
Description of the error.
Example values include |
errors.reason |
Example values include invalid , invalidParameter , and required . |
message |
Description of the error. Same as errors.message . |
HTTP Status and Error Codes
This section provides a non-exhaustive list of HTTP status and error codes that
the Cloud Storage JSON API uses. The 1xx
Informational and 2xx
Success codes are not discussed here. For more information, see Response Status
Codes in RFC 7231 §6, RFC 7232 §4,
RFC 7233 §4, RFC 7235 §3, and RFC 6585.
302—Found
Reason | Description |
---|---|
found | Resource temporarily located elsewhere according to the Location header. |
303—See Other
Reason | Description |
---|---|
mediaDownloadRedirect | When requesting a download using alt=media URL parameter, the direct URL path to use is prefixed by /download . If this is omitted, the service will issue this redirect with the appropriate media download path in the Location header. |
304—Not Modified
Reason | Description |
---|---|
notModified | The conditional request would have been successful, but the condition was false, so no body was sent. |
307—Temporary Redirect
Reason | Description |
---|---|
temporaryRedirect | Resource temporarily located elsewhere according to the Location header. Among other reasons, this can occur when cookie-based authentication is being used, e.g., when using the Storage Browser, and it receives a request to download content. |
308—Resume Incomplete
Description |
---|
Indicates an incomplete resumable upload and provides the range of bytes already received by Cloud Storage. Responses with this status do not contain a body. |
400—Bad Request
[Domain.]Reason | Description |
---|---|
badRequest | The request cannot be completed based on your current Cloud Storage settings. For example, you cannot lock a retention policy if the requested bucket doesn’t have a retention policy, and you cannot set ACLs if the requested bucket has uniform bucket-level access enabled. |
badRequestException | The retention period on a locked bucket cannot be reduced. |
cloudKmsBadKey | Bad Cloud KMS key. |
cloudKmsCannotChangeKeyName | Cloud KMS key name cannot be changed. |
cloudKmsDecryptionKeyNotFound | Resource’s Cloud KMS decryption key not found. |
cloudKmsDisabledKey | Cloud KMS key is disabled, destroyed, or scheduled to be destroyed. |
cloudKmsEncryptionKeyNotFound | Cloud KMS encryption key not found. |
cloudKmsKeyLocationNotAllowed | Cloud KMS key location not allowed. |
corsRequestWithXOrigin | CORS request contains an XD3 X-Origin header. |
customerEncryptionAlgorithmIsInvalid | Missing an encryption algorithm, or the provided algorithm is not «AE256.» |
customerEncryptionKeyFormatIsInvalid | Missing an encryption key, or it is not Base64 encoded, or it does not meet the required length of the encryption algorithm. |
customerEncryptionKeyIsIncorrect | The provided encryption key is incorrect. |
customerEncryptionKeySha256IsInvalid | Missing a SHA256 hash of the encryption key, or it is not Base64 encoded, or it does not match the encryption key. |
invalidAltValue | The value for the alt URL parameter was not recognized. |
invalidArgument | The value for one of fields in the request body was invalid. |
invalidParameter | The value for one of the URL parameters was invalid. In addition to normal URL parameter validation, any URL parameters that have a corresponding value in provided JSON request bodies must match if they are both specified. If using JSONP, you will get this error if you provide an alt parameter that is not json . |
notDownload | Uploads or normal API request was sent to a /download/* path. Use the same path, but without the /download prefix. |
notUpload | Downloads or normal API request was sent to a /upload/* path. Use the same path, but without the /upload prefix. |
parseError | Could not parse the body of the request according to the provided Content-Type. |
push.channelIdInvalid | Channel id must match the following regular expression: [A-Za-z0-9\\-_\\+/=]+ |
push.channelIdNotUnique | storage.objects.watchAll ‘s id property must be unique across channels. |
push.webhookUrlNoHostOrAddress | storage.objects.watchAll ‘s address property must contain a valid URL. |
push.webhookUrlNotHttps | storage.objects.watchAll ‘s address property must be an HTTPS URL. |
required | A required URL parameter or required request body JSON property is missing. |
resourceIsEncryptedWithCustomerEncryptionKey | The resource is encrypted with a customer-supplied encryption key, but the request did not provide one. |
resourceNotEncryptedWithCustomerEncryptionKey | The resource is not encrypted with a customer-supplied encryption key, but the request provided one. |
turnedDown | A request was made to an API version that has been turned down. Clients will need to update to a supported version. |
userProjectInvalid | The user project specified in the request is invalid, either because it is a malformed project id or because it refers to a non-existent project. |
userProjectMissing | The requested bucket has Requester Pays enabled, the requester is not an owner of the bucket, and no user project was present in the request. |
wrongUrlForUpload | storage.objects.insert must be invoked as an upload rather than a metadata. |
401—Unauthorized
[Domain.]Reason | Description |
---|---|
AuthenticationRequiredRequesterPays | Access to a Requester Pays bucket requires authentication. |
authError | This error indicates a problem with the authorization provided in the request to Cloud Storage. The following are some situations where that will occur:
|
lockedDomainExpired | When downloading content from a cookie-authenticated site, e.g., using the Storage Browser, the response will redirect to a temporary domain. This error will occur if access to said domain occurs after the domain expires. Issue the original request again, and receive a new redirect. |
required | Access to a non-public method that requires authorization was made, but none was provided in the Authorization header or through other means. |
403—Forbidden
[Domain.]Reason | Description |
---|---|
accountDisabled | The account associated with the project that owns the bucket or object has been disabled. Check the Google Cloud console to see if there is a problem with billing, and if not, contact account support. |
countryBlocked | The Cloud Storage JSON API is restricted by law from operating with certain countries. |
forbidden | According to access control policy, the current user does not have access to perform the requested action. This code applies even if the resource being acted on doesn’t exist. |
insufficientPermissions | According to access control policy, the current user does not have access to perform the requested action. This code applies even if the resource being acted on doesn’t exist. |
objectUnderActiveHold | Object replacement or deletion is not allowed due to an active hold on the object. |
retentionPolicyNotMet | Object replacement or deletion is not allowed until the object meets the retention period set by the retention policy on the bucket. |
sslRequired | Requests to this API require SSL. |
stopChannelCallerNotOwner | Calls to storage.channels.stop require that the caller own the channel. |
UserProjectAccessDenied | The requester is not authorized to use the project specified in the userProject portion of the request. The requester must have the serviceusage.services.use permission for the specified project. |
UserProjectAccountProblem | There is a problem with the project used in the request that prevents the operation from completing successfully. One issue could be billing. Check the billing page to see if you have a past due balance or if the credit card (or other payment mechanism) on your account is expired. For project creation, see the Projects page in the Google Cloud console. |
404—Not Found
Reason | Description |
---|---|
notFound | Either there is no API method associated with the URL path of the request, or the request refers to one or more resources that were not found. |
405—Method Not Allowed
Reason | Description |
---|---|
methodNotAllowed | The HTTP verb is not supported by the URL endpoint used in the request. This can happen, for example, when using the wrong verb with the /upload or /download URLs. |
408—Request Timeout
Reason | Description |
---|---|
uploadBrokenConnection | The request timed out. You should retry the request using truncated exponential backoff. |
409—Conflict
Reason | Description |
---|---|
conflict | A request to change a resource, usually a storage.*.update or storage.*.patch method, failed to commit the change due to a conflicting concurrent change to the same resource. The request can be retried, though care should be taken to consider the new state of the resource to avoid blind replacement of another agent’s changes. |
410—Gone
Description |
---|
You have attempted to use a resumable upload session or rewrite token that is no longer available. If the reported status code was not successful and you still wish to complete the upload or rewrite, you must start a new session. |
411—Length Required
Description |
---|
You must provide the Content-Length HTTP header. This error has no response body. |
412—Precondition Failed
Reason | Description |
---|---|
conditionNotMet | At least one of the pre-conditions you specified did not hold. |
orgPolicyConstraintFailed | Request violates an OrgPolicy constraint. |
413—Payload Too Large
Reason | Description |
---|---|
uploadTooLarge | This error arises if you:
|
416—Requested Range Not Satisfiable
Reason | Description |
---|---|
requestedRangeNotSatisfiable | The requested Range cannot be satisfied. |
429—Too Many Requests
[Domain.]Reason | Description |
---|---|
usageLimits.rateLimitExceeded | A Cloud Storage JSON API usage limit was exceeded. If your application tries to use more than its limit, additional requests will fail. Throttle your client’s requests, and/or retry requests using truncated exponential backoff. |
499—Client Closed Request
Description |
---|
The resumable upload was cancelled at the client’s request prior to completion. This error has no response body. |
500—Internal Server Error
Reason | Description |
---|---|
backendError | We encountered an internal error. You should retry the request using truncated exponential backoff. |
internalError | We encountered an internal error. You should retry the request using truncated exponential backoff. |
502—Bad Gateway
This error is generated when there was difficulty reaching an internal service.
It is not formatted with a JSON document. You should retry the request
using truncated exponential backoff.
503—Service Unavailable
Reason | Description |
---|---|
backendError | We encountered an internal error. You should retry the request using truncated exponential backoff. |
504—Gateway Timeout
This error is generated when there was difficulty reaching an internal service.
It is not formatted with a JSON document. You should retry the request
using truncated exponential backoff.
Уровень сложности
Сложный
Время на прочтение
12 мин
Количество просмотров 4.4K
Это главы 39 и 40 раздела «HTTP API & REST» моей книги «API». Второе издание книги будет содержать три новых раздела: «Паттерны API», «HTTP API и REST», «SDK и UI‑библиотеки». Если эта работа была для вас полезна, пожалуйста, оцените книгу на GitHub, Amazon или GoodReads. English version on Substack.
Глава 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 чем выбор в его пользу.
I have been trying to get this to work for the last couple of days, I have a metro app that needs to get and post json data to a wcf service. The getting the json data from wcf was easy enough however I have encountered 400 Bad Request error when I try to
post parsed json data. Below is my code, hopefully someone has had this problem and can help me out.
Service1.svc.cs
public string InsertSale(string saleDetails) { return ""; }
IService.cs
[WebInvoke(Method = "POST", UriTemplate = "SubmitSale", ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)] [OperationContract] string InsertSale(string saleDetails);
MainPage.xaml.cs
private async void PostToJsonServer(SalesDetails sd) { string postBody = JsonConvert.SerializeObject(sd);
//below is a sample of postBody after Serialization HttpClient client = new HttpClient(); HttpContent content = new StringContent(postBody, Encoding.UTF8, "application/json"); Uri postUrl = new Uri("http://localhost:1325/Service1.svc/JSONService/SubmitSale", UriKind.Absolute); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); HttpResponseMessage wcfResponse = await client.PostAsync(postUrl, content); MessageDialog md = new MessageDialog(wcfResponse.ToString() + "\r\n\r\n\r\n" + wcfResponse.Content.ToString(), "MESSGAE"); md.ShowAsync(); }
SalesDetails Data Contract
[DataContract] public class SalesDetails { string saleID = string.Empty; string saleType = string.Empty; double saleTotal = 0.0; double saleTax = 0.0; string saleStatus = string.Empty; int storeID = 0; DateTime dateTime = DateTime.Now; List<SalesProductDetails> saleProducts = null; List<SalesPaymentDetails> payment = null; [DataMember] public string SaleID { get { return saleID; } set { saleID = value; } } [DataMember] public string SaleType { get { return saleType; } set { saleType = value; } } [DataMember] public double SaleTotal { get { return saleTotal; } set { saleTotal = value; } } [DataMember] public double SaleTax { get { return saleTax; } set { saleTax = value; } } [DataMember] public string SaleStatus { get { return saleStatus; } set { saleStatus = value; } } [DataMember] public string Username { get; set; } [DataMember] public int StoreInformation { get { return storeID; } set { storeID = value; } } [DataMember] public int CustomerID { get; set; } [DataMember] public int NumberOfCustomers { get; set; } [DataMember] public List<SalesProductDetails> Products { get { return saleProducts; } set { saleProducts = value; } } [DataMember] public List<SalesPaymentDetails> Payments { get { return payment; } set { payment = value; } } [DataMember] public DateTime DateTime { get { return dateTime; } set { dateTime = value; } } }
postBody Serialization example
{"SaleID":"","SaleType":"SAL","SaleTotal":9.0,"SaleTax":0.81818181818181812,"SaleStatus":"Paid","Username":"ben.ringrose","StoreInformation":1,"CustomerID":0,"NumberOfCustomers":1,"Products":[{"SaleID":"","SaleDetailsID":0,"ProductPrice":2.5,"ProductQty":1.0,"TaxAmount":0.22727272727272727,"ProductCode":"2182","ProductDescription":"TEA SMALL"},{"SaleID":"","SaleDetailsID":0,"ProductPrice":3.0,"ProductQty":1.0,"TaxAmount":0.27272727272727271,"ProductCode":"2183","ProductDescription":"TEA MEDIUM"},{"SaleID":"","SaleDetailsID":0,"ProductPrice":3.5,"ProductQty":1.0,"TaxAmount":0.31818181818181818,"ProductCode":"2184","ProductDescription":"TEA LARGE"}],"Payments":[{"SaleID":"","SalePaymentID":0,"PaymentName":"Cash","PaymentAmount":9.0}],"DateTime":"2015-05-06T11:26:07.2824181+10:00"}
Entire web.config file
<?xml version="1.0"?> <configuration> <appSettings> <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" /> </appSettings> <system.web> <compilation debug="true" targetFramework="4.5" /> <httpRuntime targetFramework="4.5"/> </system.web> <system.serviceModel> <serviceHostingEnvironment multipleSiteBindingsEnabled="true" /> <services> <service behaviorConfiguration="Default" name="WcfServiceTestingAndTagging.Service1"> <!--<host> <baseAddresses> <add baseAddress = "http://10.18.4.200:8733/Service1/" /> <add baseAddress = "http://10.18.4.200/CafePOS/" /> <add baseAddress = "http://localhost:8733/Design_Time_Addresses/WcfServiceLibrary1/Service1/" /> </baseAddresses> </host>--> <endpoint address="" binding="basicHttpBinding" contract="WcfServiceTestingAndTagging.IService1" /> <endpoint address="JSONService" behaviorConfiguration="webBehavior" binding="webHttpBinding" contract="WcfServiceTestingAndTagging.IService1" /> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/> </service> </services> <client> <endpoint address="" behaviorConfiguration="webBehavior" binding="webHttpBinding" contract="WcfServiceTestingAndTagging.IService1" /> <endpoint address="basic" binding="basicHttpBinding" contract="WcfServiceTestingAndTagging.IService1" /> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/> </client> <bindings> <basicHttpBinding> <binding name="BasicBinding" maxReceivedMessageSize="2000000" maxBufferSize="2000000" transferMode="Streamed"> <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647" /> </binding> </basicHttpBinding> </bindings> <behaviors> <endpointBehaviors> <behavior name="webBehavior"> <webHttp helpEnabled="true"/> <dataContractSerializer maxItemsInObjectGraph="100000000"/> </behavior> </endpointBehaviors> <serviceBehaviors> <behavior name="Default"> <serviceMetadata httpGetEnabled="true" httpsGetEnabled="True" /> <serviceDebug includeExceptionDetailInFaults="false"/> </behavior> <behavior name=""> <serviceMetadata httpGetEnabled="True" httpsGetEnabled="True"/> <serviceDebug includeExceptionDetailInFaults="False" /> </behavior> </serviceBehaviors> </behaviors> <protocolMapping> <add binding="basicHttpsBinding" scheme="https" /> </protocolMapping> </system.serviceModel> <system.webServer> <modules runAllManagedModulesForAllRequests="true"/> <directoryBrowse enabled="true"/> </system.webServer> </configuration>
If you need any other code please let me know and I will supply
Again I hope someone can help
Cheers,
Ben
Postman – один из самых популярных инструментов в области тестирования API. Он предоставляет широкий функционал, который позволяет настраивать запросы и контролировать получаемые ответы, а также проверять работу различных HTTP-методов, кодов состояния HTTP, кукисов и других важных аспектов. Однако, иногда встречаются ошибки, которые могут сбить с толку начинающих разработчиков. Один из самых распространенных видов ошибок – это «400 Bad Request». В данной статье мы рассмотрим, что такое данная ошибка, и как ее можно избежать.
Итак, что же такое «400 Bad Request»? Эта ошибка возникает, когда клиент отправляет запрос на сервер, который сервер не в состоянии обработать. В основном, ошибка 400 связана с тем, что передаваемые параметры не соответствуют требованиям сервера. Есть несколько причин возникновения этой ошибки, которые мы рассмотрим подробно ниже.
Неправильная структура JSON-документа
JSON-документ является форматом передачи данных, который используется в RESTful API. Неправильная структура этого документа может привести к ошибке «400 Bad Request». Например, если вы используете POST-запрос для отправки данных на сервер, но вами была отправлена неправильная структура JSON-документа, то сервер не сможет получить и обработать эти данные. В таких случаях, вам необходимо внимательно проверить структуру JSON-документа, чтобы убедиться, что он соответствует требованиям сервера.
Неправильный формат параметров
Если вы отправляете параметры в виде строки или числа, а сервер ожидает массив или объект, то вы получите ошибку «400 Bad Request». Например, если ваш сервер ожидает массив значений для параметра «ids», а вы отправляете его в виде строки, то сервер не сможет обработать этот запрос. В этом случае необходимо внимательно прочитать документацию вашего API и убедиться, что параметры, которые вы отправляете, соответствуют его требованиям.
Отсутствие обязательных данных
Некоторые API требуют определенных данных для выполнения запроса. Если вы не передали какие-то из этих данных, то сервер не сможет выполнить запрос. Например, если ваш API требует передачи авторизационных данных для запроса, то если вы их не передали, вы получите ошибку «400 Bad Request». В данном случае, вам следует убедиться, что вы передаете все необходимые параметры в запросе.
Ограничения на количество запросов
Некоторые API устанавливают ограничения на количество запросов, которые могут быть выполнены в течение определенного периода времени. Если вы отправляете запросы слишком быстро или превышаете установленные лимиты, то сервер может вернуть ошибку «400 Bad Request». В этом случае вам следует выяснить ограничения, установленные API, и привести количество ваших запросов в соответствие с ними.
В заключение, ошибки «400 Bad Request» в Postman могут возникать по многим причинам, таким как неправильный формат JSON-документа, отсутствие обязательных данных, использование неправильного типа параметров или превышение установленных лимитов запросов. Если вы столкнулись с этой ошибкой, а вы не знаете, как ее решить, следует внимательно прочитать документацию вашего API или связаться с его разработчиками. Также, вы можете использовать средства Postman для тестирования и отладки API-запросов, чтобы убедиться, что они корректно работают и исправно выполняют все требования сервера.