I'm creating a project in Java with Spring Boot.
The focus is to receive an image that is converted to a stream and that my code converts this image to a pdf file and sends this pdf back as a stream.
Despite the analysis, I can't get past the beginning, receiving the stream.. .
Here you'll see a snippet of my postman call to the running project
My Controller looks like this:
#RestController
public class Controller {
#PostMapping(value = "/convert/{format}", consumes = "application/octet-stream", produces = "application/octet-stream")
#ResponseBody
public void convert(RequestEntity<InputStream> entity, HttpServletResponse response, #PathVariable String format, #RequestParam Map<String, String> params) throws IOException {
if ("pdf".equalsIgnoreCase(format)) {
PDFConverter cnv = new PDFConverter();
/*cnv.convert(entity.getBody(), response.getOutputStream(), params);*/
response.setContentType("application/octet-stream");
response.getOutputStream().println("hello binary");
} else {
// handle other formats
throw new IllegalArgumentException("illegal format: " + format);
}
}
}
What do I overlook in this case?
I found the solution, in the controller I used RequestEntity<InputStream> entity, this gave the error. After changing this to HttpServletRequest request it worked.
#RestController
public class Controller {
#RequestMapping(value="/convert/{format}", method=RequestMethod.POST)
public #ResponseBody void convert(HttpServletRequest request, HttpServletResponse response, #PathVariable String format, #RequestParam Map<String, String> params) {
try{
if ("pdf".equalsIgnoreCase(format)) {
PDFConverter cnv = new PDFConverter();
response.setContentType("application/pdf");
cnv.convert(request.getInputStream(), response.getOutputStream(), params);
} else {
// handle other formats
throw new IllegalArgumentException("illegal format: " + format);
}
} catch (IllegalArgumentException | IOException e) {
e.printStackTrace();
}
}
}
As the error message tells you already, your content-type is not valid. You expecting a different content Type than you are sending off. Might be the problem that you append the charset definition to the request.
I think you are using commons-fileupload's streaming API. This won't work if spring.http.multipart.enabled=true, due to the request being pre-processed. Can you try setting spring.http.multipart.enabled=false and also change consumes = { MediaType.MULTIPART_FORM_DATA_VALUE },
Related
I've tried the various ways given in Stackoverflow, maybe I missed something.
I have an Android client (whose code I can't change) which is currently getting an image like this:
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.connect();
Where url is the location of the image (static resource on CDN). Now my Spring Boot API endpoint needs to behave like a file resource in the same way so that the same code can get images from the API (Spring boot version 1.3.3).
So I have this:
#ResponseBody
#RequestMapping(value = "/Image/{id:.+}", method = RequestMethod.GET, consumes = MediaType.ALL_VALUE, produces = MediaType.IMAGE_JPEG_VALUE)
public ResponseEntity<byte[]> getImage(#PathVariable("id")String id) {
byte[] image = imageService.getImage(id); //this just gets the data from a database
return ResponseEntity.ok(image);
}
Now when the Android code tries to get http://someurl/image1.jpg I get this error in my logs:
Resolving exception from handler [public
org.springframework.http.ResponseEntity
com.myproject.MyController.getImage(java.lang.String)]:
org.springframework.web.HttpMediaTypeNotAcceptableException: Could not
find acceptable representation
Same error happens when I plug http://someurl/image1.jpg into a browser.
Oddly enough my tests check out ok:
Response response = given()
.pathParam("id", "image1.jpg")
.when()
.get("MyController/Image/{id}");
assertEquals(HttpStatus.OK.value(), response.getStatusCode());
byte[] array = response.asByteArray(); //byte array is identical to test image
How do I get this to behave like an image being served up in the normal way? (Note I can't change the content-type header that the android code is sending)
EDIT
Code after comments (set content type, take out produces):
#RequestMapping(value = "/Image/{id:.+}", method = RequestMethod.GET, consumes = MediaType.ALL_VALUE)
public ResponseEntity<byte[]> getImage(#PathVariable("id")String id, HttpServletResponse response) {
byte[] image = imageService.getImage(id); //this just gets the data from a database
response.setContentType(MediaType.IMAGE_JPEG_VALUE);
return ResponseEntity.ok(image);
}
In a browser this just seems to give a stringified junk (byte to chars i guess). In Android it doesn't error, but the image doesn't show.
I believe this should work:
#RequestMapping(value = "/Image/{id:.+}", method = RequestMethod.GET)
public ResponseEntity<byte[]> getImage(#PathVariable("id") String id) {
byte[] image = imageService.getImage(id);
return ResponseEntity.ok().contentType(MediaType.IMAGE_JPEG).body(image);
}
Notice that the content-type is set for ResponseEntity, not for HttpServletResponse directly.
Finally fixed this... I had to add a ByteArrayHttpMessageConverter to my WebMvcConfigurerAdapter subclass:
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
final ByteArrayHttpMessageConverter arrayHttpMessageConverter = new ByteArrayHttpMessageConverter();
final List<MediaType> list = new ArrayList<>();
list.add(MediaType.IMAGE_JPEG);
list.add(MediaType.APPLICATION_OCTET_STREAM);
arrayHttpMessageConverter.setSupportedMediaTypes(list);
converters.add(arrayHttpMessageConverter);
super.configureMessageConverters(converters);
}
In case you don't know the file/mime type you can do this.... I've done this where i take an uploaded file and replace the file name with a guid and no extension and browsers / smart phones are able to load the image no issues.
the second is to serve a file to be downloaded.
#RestController
#RequestMapping("img")
public class ImageController {
#GetMapping("showme")
public ResponseEntity<byte[]> getImage() throws IOException{
File img = new File("src/main/resources/static/test.jpg");
return ResponseEntity.ok().contentType(MediaType.valueOf(FileTypeMap.getDefaultFileTypeMap().getContentType(img))).body(Files.readAllBytes(img.toPath()));
}
#GetMapping("thing")
public ResponseEntity<byte[]> what() throws IOException{
File file = new File("src/main/resources/static/thing.pdf");
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=" +file.getName())
.contentType(MediaType.valueOf(FileTypeMap.getDefaultFileTypeMap().getContentType(file)))
.body(Files.readAllBytes(file.toPath()));
}
}
UPDATE in java 9+ you need to add compile 'com.sun.activation:javax.activation:1.2.0' to your dependencies this has also been moved or picked up by jakarta.see this post
Using Apache Commons, you can do this and expose the image on an endpoint
#RequestMapping(value = "/image/{imageid}",method= RequestMethod.GET,produces = MediaType.IMAGE_JPEG_VALUE)
public #ResponseBody byte[] getImageWithMediaType(#PathVariable int imageid) throws IOException {
InputStream in = new ByteArrayInputStream(getImage(imageid));
return IOUtils.toByteArray(in);
}
All images will be served at endpoint /image/{imageid}
I am writing a spring boot app which has REST apis (using spring mvc framework) that stream audio/video to HTML5 player on the browser. These apis support range requests for the content.
I have run into an issue where the HTML5 video player complains with error ERR_CONTENT_LENGTH_MISMATCH periodically during streaming.
It seems that bytes received from server do not match bytes advertised by server in Content-Length header.
Please advise what could be the root cause of this.
Things that I have researched so far that could potentially solve the issue but haven't in my case:
No buffering in response.
No apache in front of tomcat.
Here is my code:
#Api("Player API")
#RestController
public class PlayerController {
#Autowired
FetchAssetService fetchAssetService;
#ApiOperation("Get video")
#RequestMapping(value = "player/video/{packageId}/{username}", method = RequestMethod.GET)
public ResponseEntity<StreamingResponseBody> getProxy(#RequestHeader(value="Range", required=false) String range, #PathVariable Long packageId, #PathVariable String username) throws Exception {
Optional<Stream> videoAssetMetaData = fetchAssetService.fetchVideoAssetMetaData(packageId);
if (!videoAssetMetaData.isPresent()) {
throw new AssetNotFoundException("Video asset not found in MPL for package: "+packageId);
}
HttpHeaders httpHeaders = new HttpHeaders();
HttpStatus status = HttpStatus.OK;
Optional<AssetRange> optionalAssetRange = AssetRange.create(range,videoAssetMetaData.get().getLength());
if (optionalAssetRange.isPresent()) {
if (optionalAssetRange.get().isSatisfiable()) {
setSuccessRangeHeaders(httpHeaders,optionalAssetRange.get());
status = HttpStatus.PARTIAL_CONTENT;
} else {
setErrorRangeHeaders(httpHeaders,optionalAssetRange.get());
status = HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE;
return new ResponseEntity(null,httpHeaders,status);
}
}
setContentHeaders(httpHeaders, “video.mp4");
try {
return new ResponseEntity(fetchAssetService.getStreamingResponseBody(packageId,videoAssetMetaData.get(),optionalAssetRange,username),
httpHeaders,
status);
} catch (Exception ex) {
log.error("Exception while video streaming: package={}, user={}, range={}",packageId,username,range,ex);
throw ex;
}
}
private void setContentHeaders(HttpHeaders httpHeaders, String fileName) {
httpHeaders.add(HttpHeaders.ACCEPT_RANGES,"bytes");
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
httpHeaders.add(HttpHeaders.CONTENT_DISPOSITION,"attachment; filename="+ fileName);
}
private void setSuccessRangeHeaders(HttpHeaders httpHeaders, AssetRange range) {
httpHeaders.add(HttpHeaders.CONTENT_LENGTH, Long.toString(range.getRangeLength()));
httpHeaders.add(HttpHeaders.CONTENT_RANGE, String.format("bytes %d-%d/%d", range.getStart(), range.getEnd(), range.getTotalLength()));
}
private void setErrorRangeHeaders(HttpHeaders httpHeaders, AssetRange range) {
httpHeaders.add(HttpHeaders.CONTENT_RANGE, String.format("bytes */%d", range.getTotalLength()));
}
#ExceptionHandler(AssetNotFoundException.class)
#ResponseStatus(HttpStatus.NOT_FOUND)
public String handleAppException(AssetNotFoundException ex) {
return ex.getMessage();
}
#ExceptionHandler(Exception.class)
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleAppException(Exception ex) {
return ex.getMessage();
}
}
Best guess,
in setSuccessRangeHeaders, you are setting the content length to a range value rather than the actual content length of your response.
try not setting content_length at all or try setting it more accurately.
this might help:
How to set content length as long value in http header in java?
My problem is that I am getting the wrong sized file on the client side. Here is my #Controller ...
#RequestMapping(value = "/download/{id}", method = RequestMethod.GET)
public ResponseEntity<?> download(final HttpServletRequest request,
final HttpServletResponse response,
#PathVariable("id") final int id) throws IOException {
try {
// Pseudo-code for retrieving file from ID.
Path zippath = getZipFile(id);
if (!Files.exists(zippath)) {
throw new IOException("File not found.");
}
ResponseEntity<InputStreamResource> result;
return ResponseEntity.ok()
.contentLength(Files.size(zippath))
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(new InputStreamResource(new FileInputStream(zippath.toFile())));
} catch (Exception ex) {
// ErrorInfo is another class, unimportant
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorInfo(ex));
}
}
... and here is my client-side code using angular-file-saver ...
$http({url: "export/download/" + exportitem.exportId, withCredentials: true})
.then(function(response) {
function str2bytes(str) {
var bytes = new Uint8Array(str.length);
for (var i=0; i<str.length; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes;
}
var blob = new Blob([str2bytes(response.data)], {type: 'application/octet-stream'});
FileSaver.saveAs(blob, "download.zip");
}, $exceptionHandler);
The original file is 935673 bytes but response.data is 900728 and passing it through the transformation to Uint8Array results in a Blob that is 900728 in size as well. Either way, the resulting saved file is 900728 bytes (34945 bytes shy). Also it is not quite the same in what gets written. It seems to slightly get bloated but then the last part just seems to be truncated. Any ideas what I might be doing wrong?
UPDATE
I just updated my controller method to be the following and got the exact same result. Grrr.
#RequestMapping(value = "/download/{id}", method = RequestMethod.GET)
public void download(final HttpServletRequest request,
final HttpServletResponse response,
#PathVariable("id") final int id) throws IOException {
// Pseudo-code for retrieving file from ID.
Path zippath = getZipFile(id);
if (!Files.exists(zippath)) {
throw new IOException("File not found.");
}
response.setContentType("application/zip");
response.setHeader("Content-Disposition",
"attachment; filename=download.zip");
InputStream inputStream = new FileInputStream(zippath.toFile());
org.apache.commons.io.IOUtils.copy(inputStream, response.getOutputStream());
response.flushBuffer();
inputStream.close();
}
So the problem turned out to be angular's $http service. I also tried jQuery's ajax method. Both gave the same result. If I instead use the native XMLHttpRequest it works correctly. So the Java code was sound. I first verified this by exposing the file directly to the internet and then both using curl and directly accessing in the browser I managed to download the file of the correct size. Then I found this solution so that I could also download the file via javascript.
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = "blob";
xhr.withCredentials = true;
xhr.onreadystatechange = function (){
if (xhr.readyState === 4) {
var blob = xhr.response;
FileSaver.saveAs(blob, filename);
}
};
xhr.send();
Why does angular or jQuery give the wrong result? I still don't know but if anyone wishes to give an answer that uses those it would be appreciated.
responseType: blob
did the trick for a zip file
Angular 2 +
this.http.get('http://localhost:8080/export', { responseType: ResponseContentType.Blob })
.subscribe((res: any) => {
const blob = new Blob([res._body], { type: 'application/zip' });
saveAs(blob, "fileName.zip");
i just stumbled over the 'responseType' in $http requests, you are probably looking for 'blob': https://docs.angularjs.org/api/ng/service/$http#usage
I'm using the following exception handler in Spring 4.0.3 to intercept exceptions and display a custom error page to the user:
#ControllerAdvice
public class ExceptionHandlerController
{
#ExceptionHandler(value = Exception.class)
public ModelAndView handleError(HttpServletRequest request, Exception e)
{
ModelAndView mav = new ModelAndView("/errors/500"));
mav.addObject("exception", e);
return mav;
}
}
But now I want a different handling for JSON requests so I get JSON error responses for this kind of requests when an exception occurred. Currently the above code is also triggered by JSON requests (Using an Accept: application/json header) and the JavaScript client doesn't like the HTML response.
How can I handle exceptions differently for HTML and JSON requests?
The ControllerAdvice annotation has an element/attribute called basePackage which can be set to determine which packages it should scan for Controllers and apply the advices. So, what you can do is to separate those Controllers handling normal requests and those handling AJAX requests into different packages then write 2 Exception Handling Controllers with appropriate ControllerAdvice annotations. For example:
#ControllerAdvice("com.acme.webapp.ajaxcontrollers")
public class AjaxExceptionHandlingController {
...
#ControllerAdvice("com.acme.webapp.controllers")
public class ExceptionHandlingController {
The best way to do this (especially in servlet 3) is to register an error page with the container, and use that to call a Spring #Controller. That way you get to handle different response types in a standard Spring MVC way (e.g. using #RequestMapping with produces=... for your machine clients).
I see from your other question that you are using Spring Boot. If you upgrade to a snapshot (1.1 or better in other words) you get this behaviour out of the box (see BasicErrorController). If you want to override it you just need to map the /error path to your own #Controller.
As you have the HttpServletRequest, you should be able to get the request "Accept" header. Then you could process the exception based on it.
Something like:
String header = request.getHeader("Accept");
if(header != null && header.equals("application/json")) {
// Process JSON exception
} else {
ModelAndView mav = new ModelAndView("/errors/500"));
mav.addObject("exception", e);
return mav;
}
Since i didn't find any solution for this, i wrote some code that manually checks the accept header of the request to determine the format. I then check if the user is logged in and either send the complete stacktrace if he is or a short error message.
I use ResponseEntity to be able to return both JSON or HTML like here.
Code:
#ExceptionHandler(Exception.class)
public ResponseEntity<?> handleExceptions(Exception ex, HttpServletRequest request) throws Exception {
final HttpHeaders headers = new HttpHeaders();
Object answer; // String if HTML, any object if JSON
if(jsonHasPriority(request.getHeader("accept"))) {
logger.info("Returning exception to client as json object");
headers.setContentType(MediaType.APPLICATION_JSON);
answer = errorJson(ex, isUserLoggedIn());
} else {
logger.info("Returning exception to client as html page");
headers.setContentType(MediaType.TEXT_HTML);
answer = errorHtml(ex, isUserLoggedIn());
}
final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
return new ResponseEntity<>(answer, headers, status);
}
private String errorHtml(Exception e, boolean isUserLoggedIn) {
String error = // html code with exception information here
return error;
}
private Object errorJson(Exception e, boolean isUserLoggedIn) {
// return error wrapper object which will be converted to json
return null;
}
/**
* #param acceptString - HTTP accept header field, format according to HTTP spec:
* "mime1;quality1,mime2;quality2,mime3,mime4,..." (quality is optional)
* #return true only if json is the MIME type with highest quality of all specified MIME types.
*/
private boolean jsonHasPriority(String acceptString) {
if (acceptString != null) {
final String[] mimes = acceptString.split(",");
Arrays.sort(mimes, new MimeQualityComparator());
final String firstMime = mimes[0].split(";")[0];
return firstMime.equals("application/json");
}
return false;
}
private static class MimeQualityComparator implements Comparator<String> {
#Override
public int compare(String mime1, String mime2) {
final double m1Quality = getQualityofMime(mime1);
final double m2Quality = getQualityofMime(mime2);
return Double.compare(m1Quality, m2Quality) * -1;
}
}
/**
* #param mimeAndQuality - "mime;quality" pair from the accept header of a HTTP request,
* according to HTTP spec (missing mimeQuality means quality = 1).
* #return quality of this pair according to HTTP spec.
*/
private static Double getQualityofMime(String mimeAndQuality) {
//split off quality factor
final String[] mime = mimeAndQuality.split(";");
if (mime.length <= 1) {
return 1.0;
} else {
final String quality = mime[1].split("=")[1];
return Double.parseDouble(quality);
}
}
The trick is to have a REST controller with two mappings, one of which specifies "text/html" and returns a valid HTML source. The example below, which was tested in Spring Boot 2.0, assumes the existence of a separate template named "error.html".
#RestController
public class CustomErrorController implements ErrorController {
#Autowired
private ErrorAttributes errorAttributes;
private Map<String,Object> getErrorAttributes( HttpServletRequest request ) {
WebRequest webRequest = new ServletWebRequest(request);
boolean includeStacktrace = false;
return errorAttributes.getErrorAttributes(webRequest,includeStacktrace);
}
#GetMapping(value="/error", produces="text/html")
ModelAndView errorHtml(HttpServletRequest request) {
return new ModelAndView("error.html",getErrorAttributes(request));
}
#GetMapping(value="/error")
Map<String,Object> error(HttpServletRequest request) {
return getErrorAttributes(request);
}
#Override public String getErrorPath() { return "/error"; }
}
References
ModelAndView -- return type for HTML
DefaultErrorAttributes -- data used to render HTML template (and JSON response)
BasicErrorController.java -- Spring Boot source from which this example was derived
The controlleradvice annotation has several properties that can be set, since spring 4. You can define multiple controller advices applying different rules.
One property is "annotations. Probably you can use a specific annotation on the json request mapping or you might find another property more usefull?
Use #ControllerAdvice
Let the exception handler send a DTO containing the field errors.
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public ValidationErrorDTO processValidationError(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
return processFieldErrors(fieldErrors);
}
This code is of this website:http://www.petrikainulainen.net/programming/spring-framework/spring-from-the-trenches-adding-validation-to-a-rest-api/
Look there for more info.
When I try using standard servlet approach, in my browser the popup window shows up asking me whether to open .xls file or save it.
I tried the exactly same code via JAX-RS and the browser popup won't show up somehow. Has anyone encounter this?
JAX-RS way that won't display popup:
#Path("excellaTest")
public class ExcellaTestResource {
#Context
private UriInfo context;
#Context
private HttpServletResponse response;
#Context
private HttpServletRequest request;
public ExcellaTestResource() {
}
#Path("horizontalProcess")
#GET
//#Produces("application/vnd.ms-excel")
#Produces("application/vnd.ms-excel")
public void getProcessHorizontally() {
try {
URL templateFileUrl = this.getClass().getResource("myExcelTemplate.xls");
String templateFilePath = URLDecoder.decode(templateFileUrl.getPath(), "UTF-8");
String outputFileDir = "MasatoExcelHorizontalOutput";
ReportProcessor reportProcessor = new ReportProcessor();
ReportBook outputBook = new ReportBook(templateFilePath, outputFileDir, ExcelExporter.FORMAT_TYPE);
ReportSheet outputSheet = new ReportSheet("myExcelSheet");
outputBook.addReportSheet(outputSheet);
reportProcessor.addReportBookExporter(new OutputStreamExporter(response));
reportProcessor.process(outputBook);
System.out.println("done!!");
}
catch(Exception e) {
System.out.println(e);
}
return;
}
}//end class
class OutputStreamExporter extends ReportBookExporter {
private HttpServletResponse response;
public OutputStreamExporter(HttpServletResponse response) {
this.response = response;
}
//ReportProcessor output()
//This method is call when ReportProcessor process() is invoked.
//The Workbook from POI API can be used to write to stream
#Override
public void output(Workbook book, BookData bookdata, ConvertConfiguration configuration) throws ExportException {
//TODO write to stream
try {
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment; filename=masatoExample.xls");
book.write(response.getOutputStream());
response.getOutputStream().close();
System.out.println("booya!!");
}
catch(Exception e) {
System.out.println(e);
}
}
}//end class
What JAX-RS framework are you using?
My guess is that your code doesn't work, because you are returning void. The framework you are using probably recognizes void as HTTP 204 No Content. This causes browser to skip the actual response body and to ignore conntent-disposition header.
As I already wrote you in a parallel thread: try to returning Response object. You can put either OutputStream or byte[] as entity as set the content-disposition header.
I have never used class level injection for a jax-rs service. I suggest one of 2 solutions.
1) Try injecting the request and response as method arguments.
2) Output your file to a byte array output stream and return a byte array from your method instead of void.