可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I'm working on building a RESTful web service. I have read up on the principles of using HTTP for every mechanism as far as it will take you, and most of the time, like when fetching resources, it works pretty well.
But when I need to POST a new entry of some sort, in the interest of clarity and robustness, no matter what the client may do, I want to offer the particular validation errors that the new entry may have failed at. Additionally, there are specific errors where, say, the data for creating a new user is perfectly valid, but a nickname or an email address may be taken. Simply returning 409 Conflict
doesn't finely enough detail which of the nickname or the email address was taken.
So getting around this isn't rocket science: document a bunch of specific error codes and return an object with errors:
{ errors: [4, 8, 42] }
This means that in the case of unsuccessful requests, I'm not returning the resource or its key as I may be expected to by the REST philosophy. Similarly, when I return many resources, I have to frame them in some way in an array.
So my question is: would I still be providing a well-behaved RESTful web service if I standardized an envelope to use for every request, such that, for example, there's always an object like { errors, isSuccessful, content }
?
I have previously built RPC-style web services that used this, but I don't want to make something that's "almost REST". If there's going to be any point to being REST, I'd want to be as well-behaved as possible.
If the answer is "hell no", which I think it may be, I would like to hear if it's at least solving the validation problem correctly, and what a good reference for this sort of problem solving might be, because most guides I've found have only detailed the simple cases.
回答1:
HTTP is your envelope. You're doing the right thing by returning a 4** error code.
Having said that, there is nothing wrong with having a descriptive body on a response – in fact in the HTTP RFC, most of the HTTP Error codes advocate that you do return a description of why the error occurred. See 403 for example:
If the request method was not HEAD and the server wishes to make public why the request has not been fulfilled, it SHOULD describe the reason for the refusal in the entity.
So you're okay to continue to use the body of a response for a more detailed description of the error(s). If you're unsure of the specific HTTP error response to use (e.g. multiple errors), and you know that the user should not repeat the request as they just did it, I usually fall back to using 400.
回答2:
I'd say "hell yes!" (contrary to someone here who said "hell no") to an envelope! There's always some extra information that needs to be sent from some endpoints. Pagination, errorMessages, debugMessages for example. An example how facebook does it:
Response from get friends request
{
"data": [
{
"id": "68370",
"name": "Magnus"
},
{
"id": "726497",
"name": "Leon"
},
{
"id": "57034",
"name": "Gonçalo"
}
],
"paging": {
"next": "https://graph.facebook.com/v2.1/723783051/friends?fields=id,name&limit=5000&offset=5000&__after_id=enc_AeyGEGXHV9pJmWq2OQeWtQ2ImrJmkezZrs6z1WXXdz14Rhr2nstGCSLs0e5ErhDbJyQ"
},
"summary": {
"total_count": 200
}
}
Here we have pagination with the next link to request to get the next chunk of users and a summary with the total number of friends that can be fetched. However they don't always send this envelope, sometimes the data can go straight in the root of the body. Always sending the data the same way makes it much easier for clients to parse the data since they can do it the same for all endpoints. A small example of how clients can handle envelope responses:
public class Response<T> {
public T data;
public Paging paging;
public Summary summary;
}
public class Paging {
public String next;
}
public class Summary {
public int totalCount;
}
public class WebRequest {
public Response<List<User>> getFriends() {
String json = FacebookApi.getFriends();
Response<List<User>> response = Parser.parse(json);
return response;
}
}
This Response object could then be used for all endpoints by just changing List to the data that the endpoints returns.
回答3:
I think like many specific cases in REST it's up to you. I look to the web to for examples. For example when you go to a web page or URL that doesn't exist in the WWW you normally get a 404 and an HTML page that page usually has hypermedia to some resource. This hypermedia is what the service thinks you are trying to get to or may be the a home page {bookmark url}. In machine to machine REST senarios may not be using HTML as the media type, but you can still return a resource that 1) provides details on the error and 2) provides hypermedia to a valid resource
The 409 is an error code that you don't see much of in the wild WWW therefore you are kind of on your own. I use the 404 as a parallel and return a resource of errors like you are doing and also hypermedia to the resource that caused the 409 in the first place. That way if they intended to create the thing that caused the conflict in the first place they can just get it.
We did standardize on what error resources would look like so that clients would know how to consume the error. This of course is documented by following the rel in the resource.
In your specific case of "nickname or an email address" I could see using a 400 or a 409 because that is just one piece of information of the resource.
Also we don't have 1 single envelope. We use http://stateless.co/hal_specification.html and the resource is either what they asked for or an error.
HTH
回答4:
If by "I standardized an envelope to use for every request" you literally mean every request, not just the one you described, I would say don't do it. In REST we try to use HTTP juts like the Web uses it, not to built an entire new proprietary protocol on top of it like SOAP. This approach keeps REST simple and easy to use. If you are interested, I've put more related thoughts here:
http://theamiableapi.com/2012/03/04/rest-and-the-art-of-protocol-design/
This being said, it is OK to return detailed error description with a HTTP error code. You first instinct, returning 409 and additional error codes sounds pretty good to me. The reason 409 is better than the generic 400 is that the error handling path in client code is cleaner. Some unrelated errors can cause 400, so if you use 400, you will need to check if there is an entity body returned, what format is it in, etc.
回答5:
I used to resist the idea of enveloping the response due to the overhead of requireing to encapsulate each WebApi action.
Then I stumbled upon this article which does it in neat way that doesnt require any extra effort and it just works
Handler
public class WrappingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
return BuildApiResponse(request, response);
}
private static HttpResponseMessage BuildApiResponse(HttpRequestMessage request, HttpResponseMessage response)
{
object content;
string errorMessage = null;
if (response.TryGetContentValue(out content) && !response.IsSuccessStatusCode)
{
HttpError error = content as HttpError;
if (error != null)
{
content = null;
errorMessage = error.Message;
#if DEBUG
errorMessage = string.Concat(errorMessage, error.ExceptionMessage, error.StackTrace);
#endif
}
}
var newResponse = request.CreateResponse(response.StatusCode, new ApiResponse(response.StatusCode, content, errorMessage));
foreach (var header in response.Headers)
{
newResponse.Headers.Add(header.Key, header.Value);
}
return newResponse;
}
}
Envelope
Custom wrapper class
[DataContract]
public class ApiResponse
{
[DataMember]
public string Version { get { return "1.2.3"; } }
[DataMember]
public int StatusCode { get; set; }
[DataMember(EmitDefaultValue = false)]
public string ErrorMessage { get; set; }
[DataMember(EmitDefaultValue = false)]
public object Result { get; set; }
public ApiResponse(HttpStatusCode statusCode, object result = null, string errorMessage = null)
{
StatusCode = (int)statusCode;
Result = result;
ErrorMessage = errorMessage;
}
}
Register it!
in WebApiConfig.cs in App_Start
config.MessageHandlers.Add(new WrappingHandler());