I have a very simple file upload set up with Spring Boot.
I was wondering if there was an easy way to display an error page when the maximum file size is exceeded.
I have uploaded a very simple example of what I'm trying to achieve on github.
Basically, the idea is to catch the MultipartException in a global Spring exception handler:
#ControllerAdvice
public class UploadExceptionHandler {
#ExceptionHandler(MultipartException.class)
public ModelAndView handleError(MultipartException exception) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("error", exception.getMessage());
modelAndView.setViewName("uploadPage");
return modelAndView;
}
}
The controller which handles the file upload is really simple:
#RequestMapping("/")
public String uploadPage() {
return "uploadPage";
}
#RequestMapping(value = "/", method = RequestMethod.POST)
public String onUpload(#RequestParam MultipartFile file) {
System.out.println(file.getOriginalFilename());
return "uploadPage";
}
And the uploadPage.html thymeleaf template associated with it too:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
<title>Upload</title>
</head>
<body>
<div style="color: red" th:text="${error}" th:if="${error}">
Error during upload
</div>
<form th:action="#{/}" method="post" enctype="multipart/form-data">
<input type="file" id="file" name="file"/>
<button type="submit" name="save">Submit</button>
</form>
</body>
</html>
The idea is to display an error message in the same upload page when the file is too big.
It was my understanding that one would configure Spring's MultipartResolver to resolve exceptions lazily and be able to catch those exceptions at Spring's level (MVC exception handlers) but this code does not seem to help:
#Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(true);
return multipartResolver;
}
So before I resort to extreme measures like a filter or extending the MultipartResolver...
Do you know a clean way to handle those exceptions with Spring MVC?
Answer
Thanks to #rossen-stoyanchev.
Here is what I ended up doing:
#RequestMapping("uploadError")
public ModelAndView onUploadError(HttpServletRequest request) {
ModelAndView modelAndView = new ModelAndView("uploadPage");
modelAndView.addObject("error", request.getAttribute(WebUtils.ERROR_MESSAGE_ATTRIBUTE));
return modelAndView;
}
#Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
return container -> container.addErrorPages(new ErrorPage(MultipartException.class, "/uploadError"));
}
Works like a charm and feels like an elegant solution.
I updated the project on github if someone is interested.
Many thanks!
Multipart request parsing happens before a handler is selected and hence there is no #Controller and therefore no #ControllerAdvice to speak of yet. You can use an ErrorController (see http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-web-applications.html#boot-features-error-handling) for that.
BTW you don't need #RequestParam. It's enough that the argument type is MultipartFile.
Related
Im trying to make a timer of how much time a page is open via spring webflux, i had made this example work on springboot but with spring mvc is not working, so i know the code does work. may i have to set up something im not aware of?
Controller:
#Controller
public class HomeController {
#RequestMapping(value = "/", method = RequestMethod.GET)
public String home(){
return "home";
}
#RequestMapping(value = "/timer", method = RequestMethod.GET, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public #ResponseBody Flux<String> startTimer(){
Instant startTime = Instant.now();
return Flux.fromStream(
Stream.generate(() -> calculateTimeMethod(startTime, Instant.now()))
).delayElements(Duration.ofSeconds(1));
}
}
JSP home.jsp :
<html>
<head>
<title> Home </title>
</head>
<body>
<label> tiempo trascurrido </label>
<label id="timer"></label>
<script>
var eventSource = new EventSource("url/timer");
eventSource.onmessage = function ( event ) {
document.getElementById("timer").innerText = event.data;
};
</script>
</body>
</html>
home page load but timer doesnt start, console output shows: Failed to load resource: the server responded with a status of 406 (No Aceptable)
I am following a tutorial that has the following registration code for HiddenHttpMethodFilter. It uses a #Bean annotation that returns a new instance:
#SpringBootApplication
public class ReactiveWebApplication {
public static void main(String[] args) {
SpringApplication.run(ReactiveWebApplication.class, args);
}
#Bean
HiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new HiddenHttpMethodFilter();
}
}
However, this doesn't seem to work for me? I have a form:
<form method="post" action="/images/Picture of me 1.png" ><input type="hidden" name="_method" value="delete"/>
<input type="submit" value="Delete" />
</form>
and the request handler:
#DeleteMapping(value = BASE_PATH + "/" + FILENAME)
public Mono<String> deleteFile(#PathVariable String filename) {
return imageService.deleteImage(filename).then(Mono.just("redirect:/"));
}
But the server does not redirect the POST request to my deleteFile method:
2018-06-18 10:47:59.486 WARN 16344 --- [ctor-http-nio-5] .a.w.r.e.DefaultErrorWebExceptionHandler : Failed to handle request [POST http://localhost:8080/images/Picture%20of%20me%201.png]: Response status 405 with reason "Request method 'POST' not supported"
If I change the #DeleteMapping to #PostMapping, then it works, which leads me to suspect that the HiddenHttpMethodFilter is not kicking in?
ok, finally figured it out. Turns out there are two possible imports:
import org.springframework.web.filter.reactive.HiddenHttpMethodFilter
or
import org.springframework.web.filter.HiddenHttpMethodFilter;
and I was choosing the wrong (2nd one). I guess when developing a reactive web project you need to import the first one. I wish they had named the classes differently. Posting this answer in case somebody else falls into the same trap.
In Spring Boot the HiddenHttpMethodFilter could be enabled using spring.mvc.hiddenmethod.filter.enabled property:
spring.mvc.hiddenmethod.filter.enabled
false
Whether to enable Spring's HiddenHttpMethodFilter.
Add it to application.properties/yaml file and set it to true:
spring.mvc.hiddenmethod.filter.enabled=true
Documentation
I tried various permutation an combination and googled as well,when I am uploading xsl file with multiplesheet i am getting following Exception-
org.springframework.web.multipart.MultipartException: Could not
parse multipart servlet request; nested exception is
org.apache.commons.fileupload.FileUploadException: Stream closed] with
root cause java.io.IOException: Stream closed
Below is my code-
<form name="upload" th:action="#{/util/uploadExcel}" action="/util/uploadExcel" enctype="multipart/form-data" method="post">
<input type="file" name="fileData" path="fileData" />
<input type="submit"/>
</form>
#Controller
#RequestMapping("/util")
public class UtilController {
#RequestMapping(value=("/uploadExcel"),headers=("content-type=multipart/*"),method= RequestMethod.POST)
public ModelAndView uploadExcel(#RequestParam("fileData") CommonsMultipartFile file,BindingResult result) {
utilService.uploadData(file);
ModelAndView modelAndView = new ModelAndView("paidAnalysis/index");
return modelAndView;
}
#Bean
public CommonsMultipartResolver multipartResolver() {
return new CommonsMultipartResolver();
}
}
MoreOver I have added dependency for 'commons-fileupload' in my project.
When I upload any csv file other than xsl I am getting-
There was an unexpected error (type=Bad Request, status=400).
Required CommonsMultipartFile parameter 'fileData' is not present
I've been trying this Spring Social Accessing Twitter Data guide. And though I've double, triple an so on checked everything I keep getting this error when I click "Connect to Twitter" button:
POST request for "https://api.twitter.com/oauth/request_token" resulted in 401 (Authorization Required); invoking error handler
Here is my code:
src/main/resources/templates/connect/twitterConnect.html
<html>
<head>
<title>Hello Twitter</title>
</head>
<body>
<h3>Connect to Twitter</h3>
<form action="/connect/twitter" method="POST">
<div class="formInfo">
<p>You aren't connected to Twitter yet. Click the button to connect this application with your Twitter account.</p>
</div>
<p><button type="submit">Connect to Twitter</button></p>
</form>
</body>
src/main/resources/templates/connect/twitterConnected.html
<html>
<head>
<title>Hello Twitter</title>
</head>
<body>
<h3>Connected to Twitter</h3>
<p>
You are now connected to your Twitter account.
Click here to see your Twitter friends.
</p>
</body>
src/main/resources/templates/hello.html
<html>
<head>
<title>Hello Twitter</title>
</head>
<body>
<h3>Hello, <span th:text="${twitterProfile.name}">Some User</span>!</h3>
<h4>These are your friends:</h4>
<ul>
<li th:each="friend:${friends}" th:text="${friend.name}">Friend</li>
</ul>
</body>
src/main/java/hello/HelloController.java
#Controller
#RequestMapping("/")
public class HelloController {
private Twitter twitter;
private ConnectionRepository connectionRepository;
#Inject
public HelloController(Twitter twitter, ConnectionRepository connectionRepository) {
this.twitter = twitter;
this.connectionRepository = connectionRepository;
}
#RequestMapping(method=RequestMethod.GET)
public String helloTwitter(Model model) {
if (connectionRepository.findPrimaryConnection(Twitter.class) == null) {
return "redirect:/connect/twitter";
}
model.addAttribute(twitter.userOperations().getUserProfile());
CursoredList<TwitterProfile> friends = twitter.friendOperations().getFriends();
model.addAttribute("friends", friends);
return "hello";
}
}
src/main/java/hello/Application.java
#Configuration
#EnableAutoConfiguration
#ComponentScan
public class Application {
/*
* SPRING BOOTSTRAP MAIN
*/
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
I ran into the same problem.
After some investigation I found out the problem is in the callback url. Spring social sets this to yourap/signin/twitter. For facebook and linkedin this is fine, but for some reason twitter nees a filled in callback url in the application settings as well.
So the solution: Go to your twitter application settings, and fill in a callback url (this doesn't even have to be the actual callback url your going to use, just fill in any url!). Most likely this is the cause of your problems.
What does your application.properties file look like? It should have entries for spring.social.twitter.appId and spring.social.twitter.appSecret, populated with values you get when registering your application with Twitter.
Be sure that you have those in application.properties and that there are no curly-braces around the values (the guide's text shows curly-braces, but those are meant as placeholders, not something you should actually have in application.properties).
In my situation I have a controller class with Spring Data and validator of Spring MVC which handle form data with a template made with Thymeleaf 2.1.1
I use Spring Framework 3.2.8 and Spring Security 3.2.3.
Controller
#RequestMapping(value = "/save", method = {RequestMethod.POST, RequestMethod.GET })
public String save(#RequestParam("curriculumVitae") MultipartFile curriculumVitae, #Valid #ModelAttribute("user") User user, BindingResult result, Model model, Pageable pageable) {
try {
user.setCurriculumVitae(curriculumVitae.getBytes());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
userRepository.save(user);
return "/crudUser";
View
<form id="myform"
action="#"
th:action="#{${url}+'/save'+'?' + ${_csrf.parameterName} + '=' + ${_csrf.token}}"
th:object="${user}"
th:method="post"
enctype="multipart/form-data">
<div class="col-sm-2 col-lg-2">
<div th:class="form-group" th:classappend="${#fields.hasErrors('curriculumVitae')}? has-error">
<label class="control-label" scope="col"
th:text="#{crudUser.curriculumVitae}">Curriculum Vitae</label>
<input class="form-control input-sm" type="file" th:field="*{curriculumVitae}" name="curriculumVitae" />
</div>
</div>
</form>
The error displayed in my log is:
Failed to convert property value of type org.springframework.web.multipart.commons.CommonsMultipartFile to required type byte[] for property curriculumVitae; nested exception is java.lang.IllegalArgumentException: Cannot convert value of type [org.springframework.web.multipart.commons.CommonsMultipartFile] to required type [byte] for property curriculumVitae[0]: PropertyEditor [org.springframework.beans.propertyeditors.CustomNumberEditor] returned inappropriate value of type [org.springframework.web.multipart.commons.CommonsMultipartFile]
Thanks in advance for who will help me.
SOLVED!
In the stacktrace I have forget of set the maxUploadSize in the MultiPartResolver in my java configuration file with a value more high then default.
#Bean
public MultipartResolver multipartResolver() {
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
multipartResolver.setMaxUploadSize(500000000);
return multipartResolver;
}