I'm having a weird issue where if my servlet sends a 200 ok http status, it works as intended, and the client gets a 200 status as expected. But, if my servlet returns a 404 status, tomcat seems to change it into a 403 status. This doesn't happen if I use the http get method. I haven't tested put or post.
I want to make it very clear that my servlets doDelete method gets executed just fine. It's just that the status code returned to the browser gets changed.
I'll provide a minimal testcase demonstrating the issue.
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
#WebServlet("/api/test403/*")
public class Test403 extends HttpServlet {
public void doDelete(HttpServletRequest request, HttpServletResponse response) {
try {
String p = request.getParameter("send404");
if (p != null && "1".equals(p)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "not found.");
} else {
response.sendError(HttpServletResponse.SC_OK, "ok.");
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static final long serialVersionUID = 1L;
}
then i test via the following urls
myproject/api/test403?send404=1
myproject/api/test403?send404=0
What could cause this behavior? I'm not overly familiar the whole servlet/container architecture. I'm only experiencing this issue on 1 server which uses tomcat 7.0.41. I tried another server, which didn't exhibit this behavior.
edit - Per request, here's some output from the network panel in chrome. I used ajax to initiate this particular request:
Request Headers
DELETE /xxxxx HTTP/1.1
Host: xxxxx
Connection: keep-alive
Origin: xxx
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Referer: xxx
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Cookie: xxx
Response Headers
HTTP/1.1 403 Forbidden
Server: Apache-Coyote/1.1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Content-Encoding: gzip
Vary: Accept-Encoding
Date: Wed, 16 Apr 2014 02:30:32 GMT
I didn't remove any headers, although I anonymized some values.
A combination of...
an http DELETE request
calling HttpServletResponse.sendError(status, message) to send the 404 from your servlets doDelete() method
configuring a custom 404 error handler page(eg, via the <error-page> directive in web.xml)
Keeping the default value of readonly = true for your context
will cause the client to receive a 403 status instead of the 404 you though you sent.
A request to a servlet can service an http delete request without needing readonly to be false, but a request to a file cannot. What happens is that when you call sendError(), tomcat will try to find a custom error page that matches up with whatever http status you specified. In this case, it found one (/my404.html), and so in order to process it, it basically restarts the entire request routing/dispatching process, including running all the filters on the request. But, this time, since it's a static file request, it comes across a built in filter that looks for http the DELETE method, and then checks if readonly = false. If it's not, the request is rejected, and it changes the response status to 403 forbidden because you're not allowed to delete the static file named /my404.html.
A sensible workaround is to use HttpServletResponse.setStatus(status) instead of HttpServletResponse.sendError(status, message), so that tomcat doesn't try to find an error page. As mentioned by #BogdanZurac , you may also need to send a brief response body (i.e., "oops Error 404") in addition to setting the status to prevent it from seeking the custom error page.
Related
The Response returned by #ExceptionHandler for MaxUploadSizeExceededException in #ControllerAdvice can't be shown in swagger-ui.html.
I am using Spring Boot for my RESTful Application. And using swagger-ui to use the RESTful APIs. One of my RESTful API is upload file, using Spring's MultipartFile.
The file size is limited, and when the uploaded file size exceeds the limit, there will be an exception as below thrown.
[Request processing failed; nested exception is org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size exceeded...
To handle this exception to return an customized error message to client, I used the #ExceptionHandler in #ControllerAdvice to return the response. Below is my code.
#ControllerAdvice
public class GlobalControllerExceptionHandler{
#ExceptionHandler(MaxUploadSizeExceededException.class)
#ResponseBody
public ResponseEntity<?> maxUploadSizeExceededException(MaxUploadSizeExceededException e) {
String bodyJson = "{\"ErrorMessage\":\"Maximum upload size exceeded.\"}";
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).contentType(MediaType.TEXT_PLAIN).body(bodyJson);
}
}
What I want to see in swagger-ui.html is like this:
Response Body:
{"ErrorMessage":"Maximum upload size exceeded."}
Response Code:
500
But now, I didn't see any content:
Response Body:
no content
Response Code:
0
One abnormal symptom I saw on Browser console is that, thet request header
Provisional headers shows "Provisional headers are shown":
!Provisional headers are shown
Accept: */*
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryf7WjEeoDHtIm3ly1
Origin: http://localhost:8080
Referer: http://localhost:8080/SpringApp/swagger-ui.html
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
X-Requested-With: XMLHttpRequest
I searched about "Provisional headers are shown", someone explains that, if such message shows, it means the request is actually not sent. But on my case, I actually got the requst on my Spring Application server side and I can even hit the #ErrorHandler method "maxUploadSizeExceededException".
Swagger does not parse your Java implementation.
If you want to add more information to the documentation you have to use Swagger annotations:
https://github.com/swagger-api/swagger-core/wiki/Annotations-1.5.X
In your case something like:
#ApiResponses(value = {
#ApiResponse(code = 500, message = "Maximum upload size exceeded.")
})
But you have to add this annotation to your RestController. ControllerAdvice is not considered by swagger.
I have finally figured out the cause. The key symptom "!Provisional headers are shown" generally means request has failed to send. But actually I have hit the breakpoint on GlobalControllerExceptionHandler::maxUploadSizeExceededException. So the explain for "!Provisional headers are shown" in my case should be that, request has been sent, but data is not completely posted due to MaxUploadSizeExceededException (only partial data has been uploaded). On browser, if request post is not completed, the response will be swallowed. So swagger-ui can't display any response.
If using curl command to do the file upload, I can get the expected response in the output.
I am testing my backend (Java using Jersey on tomcat server) and frontend (Angular 4 using webpack to serve) on two different ports, thus I am getting a cors access control origin block. For my get methods everything works fine and any data requested is found on the UI. Now I am testing my POST method and I keep getting the same message in the title.
My post method should persist the data sent to it and return a response with the location of the new persisted entity.
#POST
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public Response persistAccountLocation(AccountLocation entity) throws URISyntaxException {
accountLocationService.persist(entity);
JsonObject object = Json.createObjectBuilder()
.add("location", "api/v1/accounts_locations/"+entity.getLocation_id()).build();
return Response.status(Response.Status.CREATED)// 201
.entity("Location created")
.header("Access-Control-Allow-Origin","*")
.header("Access-Control-Allow-Methods", "POST,GET,PUT,DELETE")
.allow("OPTIONS")
.entity(object.toString()).build();
}
In my network tab in firefox browser I only see OPTIONS with 200 status
Host: localhost:8081
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Access-Control-Request-Method: POST
Access-Control-Request-Headers: access-control-allow-origin,content-type
Origin: http://localhost:4200
Connection: keep-alive
but after that the post is never happens. I am assuming CORS blocks it at that point. Is there somewhere else I should be allowing access control besides the resource class? I have read that typical all CORS configuration is done on server side. Totally lost. Any feedback appreciated
EDIT
public class CORSResponseFilter
implements ContainerResponseFilter {
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
MultivaluedMap<String, Object> headers = responseContext.getHeaders();
headers.add("Access-Control-Allow-Origin", "*");
headers.add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS");
headers.add("Access-Control-Allow-Headers", "X-Requested-With, Content-Type,");
}
}
I have made a a filter with slight adjustments and registered it to my app
public class JaxRsApplication extends ResourceConfig{
public JaxRsApplication() {
// register application resources - unmapped resources will throw exception
register(AccountLocationResource.class);
register(CORSResponseFilter.class);
}
Quite a good answer is provided in How to handle CORS using JAX-RS with Jersey stackoverflow question.
You have given a link to Jersey 1, but please make sure what version you are using.
This way you will not have to write
.header("Access-Control-Allow-Origin","*")
.header("Access-Control-Allow-Methods", "POST,GET,PUT,DELETE")
.allow("OPTIONS")
for every request. Filter will apply the options for you.
One more thing I have noticed
return Response.status(Response.Status.CREATED)// 201
.entity("Location created") // <- entity() call here
.header("Access-Control-Allow-Origin","*")
.header("Access-Control-Allow-Methods", "POST,GET,PUT,DELETE")
.allow("OPTIONS")
.entity(object.toString()).build(); // <- one more entity() call here (not sure what effect it may have)
So, I'm trying to create a simple (and I mean simple) POST request. Here is the class on the server-side.
#Stateless
#Path("cards")
public class CardsFacadeREST extends AbstractFacade<Cards> {
#POST
#Path("test")
#Consumes({"text/plain"})
public void createTestCard() {
Cards card = new Cards();
card.setName("Test Card");
super.create(card);
}
#GET
#Path("count")
#Produces("text/plain")
public String countREST() {
return String.valueOf(super.count());
}
}
The GET method works just fine, but the POST method isn't working for me. I'm using Chrome's Advanced Rest Client.
The URL is http://localhost:8080/dc-rest/webresources/cards/test
The type is POST
My header is like so: Content-Type: text/plain
And that's it.
I keep getting a "400: Bad Request. The request sent by the client was syntactically incorrect."
When I open the response in the JSON window, all it says is "Unexpected token <"
Here are the request headers if that makes any difference.
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.103 Safari/537.36
Origin: chrome-extension://hgmloofddffdnphfgcellkdfbfbjeloo
Content-Type: text/plain
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8
Cookie: JSESSIONID=f4c746a32b46244d422800192f04; treeForm_tree- hi=treeForm:tree:applications
Body is empty.
And the response:
X-Powered-By: Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition 4.0 Java/Oracle Corporation/1.7)
Server: GlassFish Server Open Source Edition 4.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Allow: GET,DELETE,OPTIONS,PUT,POST
Access-Control-Allow-Headers: content-type
Content-Language:
Content-Type: text/html Date: Thu, 11 Feb 2016 20:48:12 GMT
Connection: close Content-Length: 1105
Body:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><title>GlassFish Server Open Source Edition 4.0 - Error report</title><style type="text/css"><!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}HR {color : #525D76;}--></style> </head><body><h1>HTTP Status 400 - Bad Request</h1><hr/><p><b>type</b> Status report</p><p><b>message</b>Bad Request</p><p><b>description</b>The request sent by the client was syntactically incorrect.</p><hr/><h3>GlassFish Server Open Source Edition 4.0 </h3></body></html>
The 400-Bad request response is likely because the server has expressed that it expects a specific content type
#Consumes({"text/plain"}
However the client is not indicating that the post body is of this type.
To solve this, make sure that the POST request from your client contains the following HTTP header:
Content-Type: text/plain
Or, perhaps it is the case that you are not POSTing plain text and you intend to POST XML or JSON. Whatever the intended type, you just need to make sure the client and server are in agreement about it.
If an HTTP request has a body, it must have either Content-Length or Transfer-Encoding header.
With neither, the request has no body -- not even a body of length 0. If you want to send an empty body, the request should have header Content-Length: 0.
There is a semantic difference between having no body and having an empty body. The server apparently rejects the POST request with no body. (Though the request is actually syntactically valid according to RFC)
Actually, this part is not quite clear (discussion thread (don't read)). And some implementations set Content-Length: 0 for GET requests; some implementations omit Content-Length: 0 for empty POST bodies; both are wrong... Sometimes they work sometimes they don't. Welcome to the chaotic world of HTTP.
I hate it when this happens. It was something else entirely, my persistence bean is messed up. When I commented out "super.create(card)" in my POST method, everything worked (request body or no).
Still have no idea why that resulted in "400: Bad Request. The request sent by the client was syntactically incorrect."
Seems like I have another problem to figure out, but this one at least is solved.
Thank y'all for helping!
I have just begun implementing my first Drowizard project. Here's my first resource class:
#Path("/bill")
public class CommandResource {
//...
#POST
#Consumes(MediaType.APPLICATION_JSON)
#Path("/createBillForStudy")
public Response handleCreateBillForStudyCommand(CreateBillForStudyCommand cmd, #Context UriInfo uriInfo)
{
System.out.println("Creating bill for command: " + cmd);
UUID newId = billCreator.handle(cmd);
URI location = uriInfo.getBaseUriBuilder().path(CommandResource.class).path("/" + newId).build();
return Response.accepted(cmd).contentLocation(location).build();
}
}
I want to test this using Postman, but the following request leads to a 500 response and I can't figure out why:
POST /bill/createBillForStudy HTTP/1.1
Host: localhost:8080
Content-Type: application/JSON
Cache-Control: no-cache
{ "idAccount": "123", "idStudy": "456", "timeStamp": "2014-01-01" }
This is what I see in the Dropwizard console:
ERROR [2015-05-07 16:43:08,558] org.glassfish.jersey.message.internal.WriterInterceptorExecutor: MessageBodyWriter not found for media type=application/xml, type=class com.statista.billing.domain.commands.CreateBillForStudyCommand, genericType=class com.statista.billing.domain.commands.CreateBillForStudyCommand.
0:0:0:0:0:0:0:1 - - [07/Mai/2015:16:43:08 +0000] "POST /bill/createBillForStudy/ HTTP/1.1" 500 332 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36" 8
To me this sounds as if the Content-Type header is wring or missing, but as you can see above, it's correctly set to "application/JSON".
Any ideas?
Response.accepted(Object). This will return the representation to the client. In order to determine the Content-Type of the response, we need to specify it, it it should be specified by the Accept request header.
If you are going to return a representation, you should always make sure to specify the formats supported, using the #Produces annotation. If you wan to return JSON (the same way you are accepting JSON, then just use
#Produces(MediaType.APPLICATION_JSON)
The current problem is that the response is trying to me marshalled to the XML, as specified by the error
MessageBodyWriter not found for media type=application/xml,
type=class com.s.b.d.c.CreateBillForStudyCommand
It's default to XMl, since you haven't specified in a #Produces or the client set no Accept header. If there was an Accept: application/json, then without the #Produces, it would look for the MessageBodyWriter to handle JSON.
As an aside...
accepted (202 Accepted) means
The request has been accepted for processing, but the processing has not been completed.
You should instead use created (201 Created):
The request has been fulfilled and resulted in a new resource being created. The newly created resource can be referenced by the URI(s) returned in the entity of the response, with the most specific URI for the resource given by a Location header field.
With created, you don't need to explicitly call location, the URI you pass to the method will be set as the Location header. If you want to add a body, you can chain entity(body)
Edit:
Also if you don't want to send a representation in the response, you can simple call the no-arg accepted() method. This will not send out a response body, and you should no longer get the exception.
I found an inconsistency between Java's dev_appserver and the live App Engine server.
On my local development server I have a Servlet which returns:
return response.sendError(response.SC_BAD_REQUEST, "Please log in to comment");
When I access the page I get back a Status Code message in the header which is:
Status Code:400 Please log in to comment
The issue comes when I deploy this to App Engine. When accessing that same servlet I get this "Bad Request" instead of "Please log in to comment":
Status Code:400 Bad Request
The Please log in to comment Status Code message appears in the content HTML, but not in the header as it does in the development environment.
Why is this?
Edit
Here's the curl -vvvv traces for both dev_appserver and production:
dev_appserver curl trace:
> POST /add-comment HTTP/1.1
> User-Agent: Mozilla/4.0
> Host: localhost:8080
> Accept: */*
> Content-Length: 9
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/1.1 400 Please log in to comment
< Content-Type: text/html; charset=iso-8859-1
< Cache-Control: must-revalidate,no-cache,no-store
< Content-Length: 1406
< Server: Jetty(6.1.x)
Production curl trace:
> POST /add-comment HTTP/1.1
> User-Agent: Mozilla/4.0
> Host: www.xxx.org
> Accept: */*
> Content-Length: 9
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/1.1 400 Bad Request
< Content-Type: text/html; charset=utf-8
< Vary: Accept-Encoding
< Date: Thu, 18 Aug 2011 14:04:26 GMT
< Server: Google Frontend
< Cache-Control: private
< Transfer-Encoding: chunked
I would say the prod system is the correct implementation. The javadocs for sendError() say:
Sends an error response to the client using the specified status. The
server defaults to creating the response to look like an
HTML-formatted server error page containing the specified message,
setting the content type to "text/html", leaving cookies and other
headers unmodified. If an error-page declaration has been made for the
web application corresponding to the status code passed in, it will be
served back in preference to the suggested msg parameter.
If the response has already been committed, this method throws an
IllegalStateException. After using this method, the response should be
considered to be committed and should not be written to.
I highlighted a part. This says it just returns a html page with the message when possible. It doesn't say it uses it in the HTTP Status code (which I personally haven't seen anywhere as well :()
It isn't specifically a problem with sendError. The setStatus method will behave the same way. Under normal Java, both sendError and setStatus do set the status description. The issue is that the production App Engine server always sets the status description to the standard description for each code.