How is HttpServletResponse entangled with the fetch API when making a GET request for a BLOB? - spring-boot

Using Spring Boot, I am trying to implement a REST controller, which can handle a GET request asking to return a BLOB object from my database.
Googling around a little bit, and putting pieces together, I have created the following code snippet:
#GetMapping("student/pic/studentId")
public void getProfilePicture(#PathVariable Long studentId, HttpServletResponse response) throws IOException {
Optional<ProfilePicture> profilePicture;
profilePicture = profilePictureService.getProfilePictureByStudentId(studentId);
if (profilePicture.isPresent()) {
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(profilePicture.get().getPicture());
outputStream.close();
}
}
I am sending the GET request using VanillaJS and the fetch-API:
async function downloadPicture(profilePic, studentId) {
const url = "http://localhost:8080/student/pic/" + studentId;
const response = await fetch(url);
const responseBlob = await response.blob();
if (responseBlob.size > 0) {
profilePic.src = URL.createObjectURL(responseBlob);
}
}
Somehow, this works. That's great, but now I would like to understand the usage of HttpServletResponse in this context, which I am not familiar with. It seems to me that the fetch-API makes use of HttpServletResponse (maybe even creates it), since I am not creating this object or do anything with it.
What is very strange to me is that the return-type of my controller method getProfilePicture() is void, and still I am sending a response, which is most definitely not void.
Also, if the profilePicture was not found in my database, for example due to a non-existing studentId being passed, my controller-method does not do anything. But still, I am getting a response code of 200. That's why I have added the responseBlob.size > 0 part in my Javascript to check for a positive response.
Can someone explain this magic to me, please?

response.getOutputStream(); javadoc says "Returns a ServletOutputStream suitable for writing binary data in the response." It's literally the response stream and you write the picture bytes into it. It's not related to the client reading the response. Alternatively you could just return a byte array which will be automatically written into the response stream and the result will be the same.
To return a different http status code you should change the method return type to ResponseEntity<byte[]>:
#GetMapping("student/pic/studentId")
public ResponseEntity<byte[]> getProfilePicture(#PathVariable Long studentId, HttpServletResponse response) throws IOException {
Optional<ProfilePicture> profilePicture = profilePictureService.getProfilePictureByStudentId(studentId);
if (profilePicture.isPresent()) {
return ResponseEntity.ok(profilePicture.get().getPicture()); //status code 200
} else {
return ResponseEntity.notFound().build(); //status code 404
}
}
ResponseEntity is basically springs way to return different status codes/messages.
Is there a reason why you are manually downloading the image via javascript? You could just create a img element with the http link to the image and the browser will automatically display the image content: <img src="http://localhost:8080/student/pic/studentId">

Related

I use springBoot And Vue , i want send a response to vue , But response always is garbled

#Controller
#Slf4j
public class SeckillGoodsController {
#Autowired
RedisTemplate redisTemplate;
#GetMapping("/captcha")
public void verifyCode(Long userId,Long goodsId, HttpServletResponse response){
//set Header as pic
response.setContentType("image/gif");
// no cookie keep every flush is new captcha
response.setContentType("text/html;charset=UTF-8");
response.setCharacterEncoding("utf-8");
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);//never expires
//Use a util [enter image description here][1]
ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 32, 3);
redisTemplate.opsForValue().set("captcha:"+userId+":"+goodsId,captcha.text(),60, TimeUnit.SECONDS);
try {
System.out.println(response.getOutputStream().toString());
captcha.out(response.getOutputStream());
} catch (IOException e) {
log.error("Errot",e.getMessage());
}
}
}
I send a response to vue.js but use postman test the Body always is captcha, I've set the UTF-8, but it's still wrong
[1]: https://i.stack.imgur.com/04RKS.png
this has nothing to do with Spring Boot.
I'm not entirely sure what the ArithmeticCaptcha does but I guess it creates an image and stream it to the response stream
I don't know what you would expect... You are sending binary data (an image) so it is quite normal that you can't read it.
You are setting the content type twice. You can't do that. In addition, it seems to be png so you might want to check it out.
I guess that you want to would like to get a JSON back or similar. In that case, you need to change your code
Here is an example:
#ResponseBody
#RequestMapping("/captcha")
public JsonResult captcha(Long userId, Long goodsId, HttpServletResponse response) throws Exception {
ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 32, 3);
String key = "captcha:"+userId+":"+goodsId
redisTemplate.opsForValue().set(key, captcha.text(), 60, TimeUnit.SECONDS);
return JsonResult.ok().put("key", key).put("image", captcha.toBase64());
}
Might need some tweaks to fit 100% your case but this will return a json with a key that is the one you probably will need to match in your next step and the image base64 encoded so it would be (almost) readable.
You can then add the base64 encoded string from the response as the src of your img tag.

How to redirect from spring ajax controller?

I have a controller with #ResponseBody annotation. What I want to do is if this user doesn't exists process user's Id and return a json object. If exists redirect to user page with userInfo. Below code gives ajax error. Is there any way to redirect to user page with userInfo?
#RequestMapping(value = "/user/userInfo", method = {RequestMethod.GET})
#ResponseBody
public String getUserInfo(HttpServletRequest request, HttpServletResponse response, ModelMap modelMap) {
if(...){
.....//ajax return
}else{
modelMap.addAttribute("userInfo", userInfoFromDB);
return "user/user.jsp";
}
}
Well, this method is annotated with #ResponseBody. That means that the String return value will be the body of the response. So here you are just returning "user/user.jsp" to caller.
As you have full access to the response, you can always explicitely do a redirect with response.sendRedirect(...);. It is even possible to explicitely ask Spring to pass userInfoFromDB as a RedirectAttribute through the flash. You can see more details on that in this other answer from me (this latter is for an interceptor, but can be used the same from a controller). You would have to return null to tell spring that the controller code have fully processed the response. Here it will be:
...
}else{
Map<String, Object> flash = RequestContextUtils.getOutputFlashMap(request);
flash.put("userInfo", userInfoFromDB);
response.redirect(request.getContextPath() + "/user/user.jsp");
return null;
}
...
The problem is that the client side expects a string response that will not arrive and must be prepared to that. If it is not, you will get an error client side. The alternative would then be not to redirect but pass a special string containing the next URL:
...
}else{
Map<String, Object> flash = RequestContextUtils.getOutputFlashMap(request);
flash.put("userInfo", userInfoFromDB); // prepare the redirect attribute
return "SPECIAL_STRING_FOR_REDIRECT:" + request.getContextPath() + "/user/user.jsp");
}
and let the javascript client code to process that response and ask for the next page.

How to upload just an image using Retrofit 2.0

Trying to upload an image and it keeps sending as just bytes, not an image file. This is a very simple call, I don't need to send any params other than the image itself. I don't know how to format logs so I won't post the error here unless requested to.
The service:
public interface FileUploadService {
#Multipart
#POST("upload_profile_picture")
Call<ResponseBody> uploadProfilePicture(#Part("profile_picture") RequestBody file);
}
The call being made (a file is generated earlier, had to remove this code because SO needs the post to be mainly words..dumb..):
// Generate the service from interface
FileUploadService service = ServiceGenerator.createService(FileUploadService.class, this);
// Create RequestBody instance from file
RequestBody requestFile =
RequestBody.create(MediaType.parse("image/*"), imageFile);
Log.d(LOG_TAG, "formed file");
// finally, execute the request
Call<ResponseBody> call = service.uploadProfilePicture(requestFile);
Log.d(LOG_TAG, "sending call");
call.enqueue(new Callback<ResponseBody>() {
#Override
public void onResponse(Call<ResponseBody> call,
Response<ResponseBody> response) {
Log.d(LOG_TAG, "success");
Log.d(LOG_TAG, response.toString());
}
#Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.d(LOG_TAG, "failure");
Log.e(LOG_TAG, t.getMessage());
}
});
Is the issue with the MediaType.parse method? I've tried "multipart/form-data", "image/jpeg", and the above as well and nothing has worked.
The server team has said they are receiving the call, just as bytes and no image file.
I keep getting a 400 because it's sending all bytes. How can I just send this? Do I need to send as a multipart or what? From what I've seen, you just need to tag the param in the method with #Body and do the above and it should all work. Can anybody tell me why this is happening? Thanks!
This is a known issue in Retrofit 2.
Edit: Support for OkHttp's MultipartBody.Part has been added in the final 2.0 release.
In order to get it working, you need to change your interface a little bit first:
#Multipart
#POST("upload_profile_picture")
Call<ResponseBody> uploadProfilePicture(#Part MultipartBody.Part file);
Then you have to create the Part and make the call like this:
MultipartBody.Part file = MultipartBody.Part.createFormData(
"file",
imageFile.getName(),
RequestBody.create(MediaType.parse("image/*"), imageFile));
Call<ResponseBody> call = service.uploadProfilePicture(file);

Empty request body gives 400 error

My Spring controller method looks something like this:
#RequestMapping(method=RequestMethod.PUT, value="/items/{itemname}")
public ResponseEntity<?> updateItem(#PathVariable String itemname, #RequestBody byte[] data) {
// code that saves item
}
This works fine except when a try to put a zero-length item, then I get an HTTP error: 400 Bad Request. In this case my method is never invoked. I was expecting that the method should be invoked with the "data" parameter set to a zero-length array.
Can I make request mapping work even when Content-Length is 0?
I am using Spring framework version 4.1.5.RELEASE.
Setting a new byte[0] will not send any content on the request body. If you set spring MVC logs to TRACE you should see a message saying Required request body content is missing as a root cause of your 400 Bad Request
To support your case you should make your #RequestBody optional
#RequestMapping(method=RequestMethod.PUT, value="/items/{itemname}")
public ResponseEntity<?> updateItem(#PathVariable String itemname, #RequestBody(required = false) byte[] data) {
// code that saves item
}

Send Status code and message in SpringMVC

I have the following code in my web application:
#ExceptionHandler(InstanceNotFoundException.class)
#ResponseStatus(HttpStatus.NO_CONTENT)
public ModelAndView instanceNotFoundException(InstanceNotFoundException e) {
return returnErrorPage(message, e);
}
Is it possible to also append a status message to the response? I need to add some additional semantics for my errors, like in the case of the snippet I posted I would like to append which class was the element of which the instance was not found.
Is this even possible?
EDIT: I tried this:
#ResponseStatus(value=HttpStatus.NO_CONTENT, reason="My message")
But then when I try to get this message in the client, it's not set.
URL u = new URL ( url);
HttpURLConnection huc = (HttpURLConnection) u.openConnection();
huc.setRequestMethod("GET");
HttpURLConnection.setFollowRedirects(true);
huc.connect();
final int code = huc.getResponseCode();
String message = huc.getResponseMessage();
Turns out I needed to activate custom messages on Tomcat using this parameter:
-Dorg.apache.coyote.USE_CUSTOM_STATUS_MSG_IN_HEADER=true
The message can be in the body rather than in header. Similar to a successful method, set the response (text, json, xml..) to be returned, but set the http status to an error value. I have found that to be more useful than the custom message in header. The following example shows the response with a custom header and a message in body. A ModelAndView that take to another page will also be conceptually similar.
#ExceptionHandler(InstanceNotFoundException.class)
public ResponseEntity<String> handle() {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("ACustomHttpHeader", "The custom value");
return new ResponseEntity<String>("the error message", responseHeaders, HttpStatus.INTERNAL_SERVER_ERROR);
}

Resources