Issues while File download - java

I'm trying to download a file from Angular UI, even after I got exception in backed code still I'm getting 200 ok as the response.
Here is the code I have :
public ResponseEntity<Object> downloadDocument(#PathVariable("docId") Long docId,
HttpServletResponse response) {
OutPutStream outputStream = null;
try {
outputStream = response.getOutputStream();
docService.downloadDocument(docId,outputStream);
return ResponseEntity.ok().contentType(MediaType.parseMediaType(MediaType.APPLICATION_OCTET_STREAM_VALUE))
.body("Success");
} catch(Exception e) {
return ResponseEntity.badRequest().body(e.getMessage());
} finally {
if (Objects.nonNull(outputStream)) {
IOUtils.closeQuietly(outputStream);
}
}
Can you please help me out what's wring here.

When you open the outputstream, the headers are sent. period.
There's no backtracking from that point on. You can't first open the outputstream and then later go: Oh, wait! No! nevermind! bad request!
Here's how it works - you pick a side and stick to it. You can either:
Handle it yourself; use response and the methods available there to set up your response; you can set headers, the return code and message, and you can obtain an outputstream for the response body, and send data that way. If you do this, you can't ALSO return a ResponseEntity!
Do NOT even add an HttpServletResponse parameter, and instead return a ResponseEntity object.
You're doing both, which is not allowed.
I'm frankly surprised; spring is a bit broken and ought to be throwing exceptions here, as it cannot possibly serve up what you're asking it to do here.
NB: Note that the type of an exception is usually more informative than the message (many exceptions don't even have a message).
Putting it all together:
public ResponseEntity<?> downloadDocument(#PathVariable("docId") Long docId) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
docService.downloadDocument(docId, baos);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(MediaType.APPLICATION_OCTET_STREAM_VALUE))
.body(baos.toByteArray());
} catch(Exception e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
'toString' defaults to printing its own type if there is no message, and its own type plus the message if there is, so you get e.g.:
NullPointerException
or
NullPointerException: parameter foo
which is what you want (versus a literal blank string in the former case, and just 'parameter foo' in the latter, which isn't particularly insightful either).
messages are generally intended not to necessarily make sense without the context of the type of the exception.
NB: This will cache the entire content of the downloaded document into the memory of the server before sending it onwards. If the document is large, this is a bad idea, but, if you want to 'stream' it, you have a pretty serious problem inherent in the HTTP protocol: Once you start sending, you've already sent the 'error status' (i.e. you already sent 200 OK), so if the document download process throws an exception halfway through, you cannot go back and send an error message. You need some sort of wire protocol where you send the body in chunks and have a 'type' code you send, so that the recipient can scan for these and knows 'this type means there's more data flowing', and 'this type means an error occured and now I can read the error description'. That all gets quite complicated. By caching it, you avoid this mess. But if that document can be very large you're going to have to deal with the above, there are no quick fixes.

Related

How to notify exception to client after HTTP code is returned

I use Java and Spring framework to create, inside a REST controller class, a method bound to GET requests.
However, the result returned by this method is sent as a stream which is fed asynchronously by another service (using InfluxDB).
Therefore, it immediately returns code 200 to the client, even though a timeout or any exception can occur afterwards.
I would like to notify the client about this.
/**
* InfluxDB service
*/
#Inject
InfluxDBService influxDBService;
/**
* #return CSV file containing the data
*/
#RequestMapping(value="/dump", method=RequestMethod.GET, produces="application/csv")
public #ResponseBody void getDump(
HttpServletResponse response,
#RequestParam(value = "app", required = false) String appFilter,
#RequestParam(value = "context", required = false) String contextFilter,
#RequestParam(value = "path", required = false) String pathRegex
) throws DataAnalysisException {
[...]
InputStream dump = influxDBService.dump( ... filters after treatment ...);
response.setContentType("application/csv");
long currentTime = System.currentTimeMillis() / 1000;
String fileName = "influxdb-dump_" + currentTime + ".csv";
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
try {
FileCopyUtils.copy(dump, response.getOutputStream());
} catch (IOException e) {
throw new DataAnalysisException("Could not get output from request results", e);
}
}
In the dump() method, an OkHttpClient creates a remote connection to an InfluxDB server and returns an InputStream of data. This client has a default timeout of 10 seconds.
If there is not too much data, everything works fine and the client downloads a CSV with correct data.
But if the InfluxDB server doesn't answer in time (too much data), then an empty CSV file is downloaded, even though HTTP code 200 is returned.
Thing is, when I debug, it goes through the FileCopyUtils.copy line which returns 200, but after 10 seconds it goes through the "throw new DataAnalysisException" catch-block. But at this time, client has already downloaded an empty CSV and got code 200.
DataAnalysisException is a custom exception returning HTTP code 500.
My question is : after the timeout, is there a way to notify the client that we actually had an issue even though he got 200 ? That could help me build an error page to notify him.
Thanks to you all.
I solved it.
Instead of FileCopyUtils.copy, I used StreamUtils.copy, which is basically the same thing, except it doesn't automatically close the input and output streams.
Then, in a catch clause, I do response.reset() then response.sendError(code, "msg"), and throw an exception.
And in a finally clause, I manually close both input and output streams.
Therefore, the CSV headers and remaining data are cleared, and when the streams close it doesn't tell the browser to download a CSV file.
Don't hesitate to contact me if you need more info or precise code.

Sending a zero-length HTTPS PUT?

I'm working with a system that, in order to make a particular service call, requires the following:
Issue an HTTP PUT command
Set the URL to some_url_here
Set the end user certificate.
Ensure that the entity body is empty and set the Content-Length headers to 0.
Here's the method I wrote to build secure connections. I've tested the GETs; they work fine. I know the problem isn't in the certificate.
public HttpsURLConnection getSecureConnection(final URL url, final String method, final int connectTimeout,
final int readTimeout) throws IOException {
Validate.notNull(sslContext);
Validate.notNull(url);
Validate.notNull(method);
Validate.isTrue(connectTimeout > 0);
Validate.isTrue(readTimeout > 0);
HttpsURLConnection connection;
try {
connection = (HttpsURLConnection) url.openConnection();
} catch (final IOException ioe) {
LOGGER.error("[CertificateLoader] Unable to open URL connection!", ioe);
throw new IOException("Unable to open URL connection!", ioe);
}
connection.setSSLSocketFactory(sslContext.getSocketFactory());
connection.setRequestMethod(method);
connection.setConnectTimeout(connectTimeout);
connection.setReadTimeout(readTimeout);
connection.setHostnameVerifier(NoopHostnameVerifier.INSTANCE);
if (method.equals("PUT")) {
connection.setRequestProperty("Content-Length", "0");
}
if (connection.getContentLength() > 0) {
Object foo = connection.getContent();
LOGGER.error("This is what's in here: " + foo.toString());
}
return connection;
}
Now, the reason for that funky if at the bottom is that when I go to make the PUT call, even though I'm not putting a body on the call directly, my logs insist I'm getting a non-zero content length. So, I added that little block to try to figure out what's in there, and lo and behold it reports the following:
This is what's in here: sun.net.www.protocol.http.HttpURLConnection$HttpInputStream#70972170
Now, that sucker's in there by default. I didn't put it in there. I didn't create that object to put in there. I just created the object as is from the URL, which I created from a String elsewhere. What I need is a way to remove that HttpInputStream object, or set it to null, or otherwise tell the code that there should be no body to this PUT request, so that my server won't reject my message as being ill-formatted. Suggestions?
Now, the reason for that funky if at the bottom is that when I go to make the PUT call, even though I'm not putting a body on the call directly, my logs insist I'm getting a non-zero content length.
The way to set a zero Content-length is as follows:
connection.setDoOutput(true); // if it's PUT or POST
connection.setRequestMethod(method);
connection.getOutputStream().close(); // send a zero length request body
It is never necessary to call connection.setRequestProperty("Content-Length", "0"). Java sets it for you. Or possibly it is omitted, in which case you may be able to ensure it via
connection.setFixedLengthStreamingMode(0);
So, I added that little block to try to figure out what's in there, and lo and behold it reports the following:
This is what's in here: sun.net.www.protocol.http.HttpURLConnection$HttpInputStream#70972170
Now, that sucker's in there by default. I didn't put it in there.
Java put it there.
I didn't create that object to put in there.
Java put it there.
I just created the object as is from the URL, which I created from a String elsewhere. What I need is a way to remove that HttpInputStream object, or set it to null, or otherwise tell the code that there should be no body to this PUT request, so that my server won't reject my message as being ill-formatted.
No it isn't. It is an input stream, not a piece of content. And it is an input stream to the content of the response, not of the request. And in any case, the server is perfectly entitled to return you content in response to your request.
Your task is to:
Get the response code and log it.
If it is >=200 and <= 299, get the connection's input stream.
Otherwise get the connection's error stream.
Whichever stream you got, read it till end of stream, and log it.
That will tell you what is really happening.
I will add that a PUT without a body is a really strange thing to do. Are you sure you've understood the requirement? 411 means Length required.

The requested route has not been mapped in Spark

I want to do something to sign up users with spark+java+hibernate+postgres
This is my code:
post("/registrar", (request, response) -> {
EntityManagerFactory emf = Persistence.
createEntityManagerFactory("compradorcitoPU");
EntityManager em = emf.createEntityManager();em.getTransaction().begin();
em.persist(u);
em.getTransaction().commit();
em.close(); return null; });
but this error shows up:
INFO spark.webserver.MatcherFilter - The requested route
[/registrarnull] has not been mapped in Spark
I had a similar problem. The items I'm returning are large and I wanted to write them out over stream. So, my software looked like this:
post("/apiserver", "application/json", (request, response) -> {
log.info("Received request from " + request.raw().getRemoteAddr());
ServerHandler handler = new ServerHandler();
return handler.handleRequest(request, response);
});
In my handler, I got the raw HttpResponse object, opened its OutputStream and wrote over it like so:
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.raw().getOutputStream(), records);
Since I knew I had written over the OutputStream what the caller had asked for at that point (or an error), I figured I could just return null. My program worked fine. Spark would route the request to my handler as expected. And, since I was writing over the raw OutputStream, I was getting back what was expected on the client side. But, I kept seeing the message '/apiserver route not defined' in my server logs.
In looking at the Spark documentation, it says:
The main building block of a Spark application is a set of routes. A route is made up of three simple pieces:
A verb (get, post, put, delete, head, trace, connect, options)
A path (/hello, /users/:name)
A callback (request, response) -> { }
Obviously Spark does not know what you wrote over the raw HttpResponse and as a web-server, you should be providing some response to callers. So, if your response is null, you haven't fulfilled the requirements of providing a callback and you get the error that there's no map found even if Spark behaved as expected otherwise. Just return a response (null is not a response, "200 OK" is) and the error will go away.
[Edit] Spelling and grammar.
do not "return null" instead return the empty string or something
As explained in the comments of this issue, SparkJava considers that returning null means the route has not been mapped and therefore it logs the error message and replies a response with 404 status.
To avoid such behaviour you have to return a String (possibly empty).
The error message will disappear and a response with the String as body and 200 status will be replied.
In my case, I had to implement the options request to please the preflight CORS check:
options("/*", (request,response)->{
String accessControlRequestHeaders = request.headers("Access-Control-Request-Headers");
if (accessControlRequestHeaders != null) {
response.header("Access-Control-Allow-Headers", accessControlRequestHeaders);
}
String accessControlRequestMethod = request.headers("Access-Control-Request-Method");
if(accessControlRequestMethod != null){
response.header("Access-Control-Allow-Methods", accessControlRequestMethod);
}
return "OK";
});

Streaming an upload with HttpClient/MultipartEntity

I've got a Tomcat instance right now that takes uploads and does some processing work on the data.
I want to replace this with a new servlet that conforms to a similar API. At first, I want this new servlet to just proxy all of the requests to the old one. They're running on separate JVMs, but on the same host.
I've been trying to use the HttpClient to proxy the upload, but it seems that the client waits for the stream to finish before it proxies the request. For large files, this causes the servlet to crash (I think it's buffering everything in memory).
Here's the code I'm currently using:
HttpPost httpPost = new HttpPost("http://localhost:8081/servlet");
String filePartName = request.getHeader("file_part_name");
_logger.info("Attaching file " + filePartName);
try {
Part filePart = request.getPart(filePartName);
MultipartEntity mpe = new MultipartEntity();
mpe.addPart(
filePartName,
new InputStreamBody(filePart.getInputStream(), filePartName)
);
httpPost.setEntity(mpe);
} catch (ServletException | IOException e) {
_logger.error("Caught exception trying to cross the streams, thanks Ghostbusters.", e);
throw new IllegalStateException("Could not proxy the request", e);
}
HttpResponse postResponse;
try {
postResponse = HTTP_CLIENT.execute(httpPost);
} catch (IOException e) {
_logger.error("Caught exception trying to cross the streams, thanks Ghostbusters.", e);
throw new IllegalStateException("Could not proxy the request", e);
}
I can't seem to figure out how to get HttpClient/HttpPost to stream the data as it comes in, instead of blocking until the first upload completes. Has anyone done something similar before? Is there an easier solution?
Thanks!
The issue lies in the way your request is processed by the Mime/Multiplart framework (the one you use to process your HTTPServletRequest, and access file parts).
The nature of a MIME/Multipart request is simple (at a high level), instead of having a traditionnal key=value content, those requests have much more complex syntax, that allows them to carry arbitrary, unstructured data (files to upload).
It basically looks like (taken from wikipedia):
Content-type: multipart/mixed; boundary="'''frontier'''"
This is a multi-part message in MIME format.
--'''frontier'''
Content-type: text/plain
This is the body of the message.
--'''frontier'''
Content-type: application/octet-stream
Content-Disposition: form-data; name="image1"
Content-transfer-encoding: base64
PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg
Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==
--'''frontier'''--
The important part to note is that parts (that are separated by the boundary '''frontier''' here) have "names" (through the Content Disposition header), then follows the content. One such request can have any number of parts.
Now of course, the most simple, straightforward way to implement the parsing of such a request is to process it till the end, detect the boundary, and create a temporary file (or in-memory cache) to hold each part, identified by name.
Seeing the framework can not know what part you will need first (you may need the second part in your servlet call before the first), it parses the whole stream, and then, gives you back the control.
Therefore your call is blocked at this line
Part filePart = request.getPart(filePartName);
Here, the framework has to wait to parse the whole MIME part, before letting you use the result (even a rethorical, super optimised parser could not both parse lazily the stream, and allow you random access to any parts of the message, you'd have to choose between the two options).
So there's not much you can do...
Except, not use the Multipart parser. I wouldn't recommend this if you're not familiar with MIME (and/or MIME libraries such as Apache James), nor confident that you are in control of your request's structure.
But if you are, then you may bypass the framework processing, and access the raw stream of the request. You'd parse the MIME structure by hand, and stop when you hit the start of the request's body, and start building your HTTP Post at this point, being carefull to actually take care of MIME level technicalities (de-base64 ? de-gzip ?, ...).
Alternatively, if you think your server crashes because of an out of memory, it may very well be possible that your framework is configured to cache contents of the multpart in memory. But if there is a way to configure it to cache to disk, then this is a possible workaround.

Handling wrong URLs

I have csv files stored on my server. If I enter the right key (which is a part of URL) I get what I want, but if the entered key was wrong my app crashes. I want to be able to catch the error.
String url="http://mysite.com/template";
url=url+et.getText().toString().toLowerCase()+".csv";
csv.setURL(url);
if(csv.checkURL()){
enterToDB();
}
else{
tv.setText("Wrong key");
}
and my CSVReader looks like:
public void setURL(String file){
try {
URL url = new URL(file);
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
success=true;
in.close();
} catch (MalformedURLException e) { success=false;} catch (IOException e) { success=false; }
}
public boolean checkURL(){
return success;
}
}
Without the complete minimal code necessary to replicate the problem, and without information about what the specific error is (like a stack trace), I'm just guessing that the setURL/checkURL routines don't exactly do what you want. They appear to assume that openStream will throw an exception if the key in the URL is wrong, but that's not the case. Even if the path or key in the URL is wrong, the HTTP server will still provide a response. The response might not be 200 OK and the response body might not include what you're looking for, but it'll still give a response, and the open stream can be used to read the response.
So, if I understand correctly, you actually want to inspect the contents of the response (including probably the HTTP status code), before deciding whether the "success" is true or false.
Thane posted some code I gave him over in How should I handle server timeouts and error code responses to an http post in Android App?. I recommend reviewing it and seeing if it provides the structure you need to handle successful and failed responses accordingly.
Making sense?

Categories