I am making a spring boot REST application. I am trying to make a multipart form upload controller which will handle a form data and a file upload together. This is my controller code at the moment :
#RequestMapping(value = "", method = RequestMethod.POST, headers="Content-Type=multipart/form-data")
#PreAuthorize("hasRole('ROLE_MODERATOR')")
#ResponseStatus(HttpStatus.CREATED)
public void createNewObjectWithImage(
/*#RequestParam(value="file", required=true) MultipartFile file,
#RequestParam(value="param_name_1", required=true) final String param_name_1,
#RequestParam(value="param_name_2", required=true) final String param_name_2,
#RequestParam(value="param_name_3", required=true) final String param_name_3,
#RequestParam(value="param_name_4", required=true) final String param_name_4,
#RequestParam(value="param_name_5", required=true) final String param_name_5*/
#ModelAttribute ModelDTO model,
BindingResult result) throws MyRestPreconditionsException {
//ModelDTO model = new ModelDTO(param_name_1, param_name_2, param_name_3, param_name_4, param_name_5);
modelValidator.validate(model, result);
if(result.hasErrors()){
MyRestPreconditionsException ex = new MyRestPreconditionsException(
"Model creation error",
"Some of the elements in the request are missing or invalid");
ex.getErrors().addAll(
result.getFieldErrors().stream().map(f -> f.getField()+" - "+f.getDefaultMessage()).collect(Collectors.toList()));
throw ex;
}
// at the moment, model has a MultipartFile property
//model.setImage(file);
modelServiceImpl.addNew(model);
}
I have tried both with the #ModelAttribute annotation and sending request parameters, but both of these methods have failed.
This is the request i am sending :
---------------------------acebdf13572468
Content-Disposition: form-data; name="file"; filename="mint.jpg"
Content-Type: image/jpeg
<#INCLUDE *C:\Users\Lazaruss\Desktop\mint.jpg*#>
---------------------------acebdf13572468
Content-Disposition: form-data; name=”param_name_1”
string_value_1
---------------------------acebdf13572468
Content-Disposition: form-data; name=”param_name_2”
string_value_2
---------------------------acebdf13572468
Content-Disposition: form-data; name=”param_name_3”
string_value_3
---------------------------acebdf13572468
Content-Disposition: form-data; name=”param_name_4”
string_value_4
---------------------------acebdf13572468
Content-Disposition: form-data; name=”param_name_5”
string_value_5
---------------------------acebdf13572468--
My application is stateless, and uses spring security with authorities.
In my security package, i have included the AbstractSecurityWebApplicationInitializer class
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
#Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
}
I also use a StandardServletMultipartResolver in my #Configuration class
And in my WebInitializer, i add this code :
MultipartConfigElement multipartConfigElement = new MultipartConfigElement("/tmp",
3 * 1024 * 1024, 6 * 1024 * 1024, 1 * 512 * 1024);
apiSR.setMultipartConfig(multipartConfigElement);
When i try to use the controller with the commented code (#RequestParams annotations), i get a 404 not found error.
And when i try to use the controller with the #ModuleAttribute annotation, the model object is empty.
I had a similar problem. When you want to send Object + Multipart. You have to (or at least I don't know other solution) make your controller like that:
public void createNewObjectWithImage(#RequestParam("model") String model, #RequestParam(value = "file", required = false) MultipartFile file)
And then: Convert String to your Object using:
ObjectMapper mapper = new ObjectMapper();
ModelDTO modelDTO = mapper.readValue(model, ModelDTO.class);
And in Postman you can send it like that:
can receive objects and files
#PostMapping(value = "/v1/catalog/create", consumes = MediaType.MULTIPART_FORM_DATA_VALUE )
public void createNewObjectWithImage(
#RequestPart ModelTO modelTO,
#RequestPart MultipartFile image)
ModelTO
public class ModelTO {
private String name;
public ModelTO() {
super();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
and curl example:
curl -X POST "https://your-url.com/v1/catalog/create" -H "accept: application/json;charset=UTF-8" -H "Content-Type: multipart/form-data" -F "image=#/pathtoimage/powerRager.jpg;type=image/jpeg" -F "modelTO={\"name\":\"White\"};type=application/json;charset=utf-8"
Postman and other software not support send application/json type for form-data params.
Related
I have a DTO that contains other DTOs and a list of multipart files. I am trying to process that DTO but I can't seem to be able to read the requst.
class TeacherDTO {
private SpecializationDto specializationDto;
private List<MultipartFile> files;
}
#PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE},
produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<Object> saveNewTeacher(#ModelAttribute #Valid TeacherDTO teacherDto){
//process request
}
When creating an example request from Swagger UI, I get the following exception:
type 'java.lang.String' to required type 'SpecializationDto' for property 'specializationDto': no matching editors or conversion strategy found
If I put #RequestBody instead of #ModelAttribute then I get
Content type 'multipart/form-data;boundary=----WebKitFormBoundaryVEgYwEbpl1bAOjAs;charset=UTF-8' not supported]
Swagger dependencies:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-data-rest</artifactId>
<version>1.5.2</version>
</dependency>
OpenAPI3.0 config:
#Configuration
public class OpenApi30Config {
private final String moduleName;
private final String apiVersion;
public OpenApi30Config(
#Value("${spring.application.name}") String moduleName,
#Value("${api.version}") String apiVersion) {
this.moduleName = moduleName;
this.apiVersion = apiVersion;
}
#Bean
public OpenAPI customOpenAPI() {
final var securitySchemeName = "bearerAuth";
final var apiTitle = String.format("%s API", StringUtils.capitalize(moduleName));
return new OpenAPI()
.addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
.components(
new Components()
.addSecuritySchemes(securitySchemeName,
new SecurityScheme()
.name(securitySchemeName)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
)
)
.info(new Info().title(apiTitle).version(apiVersion));
}
}
This seems to be an issue with how the springdoc-openapi-ui builds the form-data request. I was able to reproduce this and noticed that it sends a multipart-request like (intercepted through browser's dev-tools):
-----------------------------207598777410513073071314493349
Content-Disposition: form-data; name="specializationDto"\r\n\r\n{\r\n "something": "someValue"\r\n}
-----------------------------207598777410513073071314493349
Content-Disposition: form-data; name="files"; filename="somefile.txt"
Content-Type: application/octet-stream
<content>
-----------------------------207598777410513073071314493349
Content-Disposition: form-data; name="files"; filename="somefile.txt"
Content-Type: application/octet-stream
<content>
With that payload Spring is not able to deserialize the specializationDto, resulting in the "no matching editors or conversion strategy found" exception that you've observed. However, if you send the request through postman or curl with (note the dot-notation for the specializationDto object)
curl --location --request POST 'http://localhost:8080/upload' \
--form 'files=#"/path/to/somefile"' \
--form 'files=#"/path/to/somefile"' \
--form 'specializationDto.something="someValue"'
then Spring is able to parse it correctly. Here's my rest-mapping that will log the following as expected:
#RequestMapping(value = "/upload", method = RequestMethod.POST,
consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public void upload(#ModelAttribute TeacherDto requestDto) {
System.out.println(requestDto);
}
// logs:
TeacherDto(specializationDto=SpecializationDto(something=someValue), files=[org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile#78186ea6, org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile#461c9cbc])
I suggest you open a bug on their github page.
EDIT:
After OP opened a github ticket, here's part of the author's feedback:
[...] With spring, you can use #RequestPart spring annotation to describe
the different parts, with the related encoding media type. Note that
there is a limitation with the current swagger-ui implementation as
the encoding attribute is not respected on the request.[...]
They also provided a possible workaround, which looks like this:
#PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> saveNewTeacher( #RequestPart(value = "specializationDto") #Parameter(schema =#Schema(type = "string", format = "binary")) final SpecializationDto specializationDto,
#RequestPart(value = "files") final List<MultipartFile> files){
return null;
}
I have a encrypted string being sent by a client. I am trying to intercept the string using ContainerRequestFilter then decrypt it and set the InputStream again so that it can be used by Jackson to map to a POJO.
Illustration:
My Resource
#Path("auth")
public class AuthResource {
#POST
public Response testResource(#Auth AuthUser auth, Person person) {
System.out.println("Recieved Resource:: "+ new Gson().toJson(person));
return null;
}
}
Person.java
public class Person {
private String name;
private int age;
public Person() {};
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
My Filter
#Provider
public class MyFilter implements ContainerRequestFilter {
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
InputStream inputStream = requestContext.getEntityStream();
StringWriter writer = new StringWriter();
IOUtils.copy(inputStream, writer, "UTF-8");
String theString = writer.toString();
String decryptedMessage = "";
try {
decryptedMessage = JwtToken.decryptPayload(theString);
System.err.println("Decrypted Message: "+decryptedMessage);
} catch (Exception e) {
e.printStackTrace();
}
InputStream stream = new ByteArrayInputStream(decryptedMessage.getBytes(StandardCharsets.UTF_8));
requestContext.setEntityStream(stream);
}
}
I understand that once the InputStream is utilized it cannot be used again. But using requestContext.setEntityStream(stream); I am trying to set the InputStream again to be utilized by Jackson.
Inspite of that I am still unable to get the person object in my resource. The decryption is working fine as I have tested it using a debugger.
I get the following error: 415: Unsupported Media Type
Edit 1: I am using Adavanced Rest Client to hit the url
http://localhost:8080/auth
Header:
authorization: Basic ZXlKaGRYUm9iM0pwZW1GMGFXOXVJam9pWVcxcGRDSXNJbUZzWnlJNklraFRNalUySW4wLmUzMC5MLUtmOUxQNjFSQ21Bektob2lTR0V4bEJ3RXRUMXhrR3A3bUpIZmFSeV9FOnBhc3M=
Raw Payload:
eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiQW1pdCIsImFnZSI6MjJ9.-rO6yhYJ--3ZzVCaHFw1hF-s533foYY6vVAuyRh3Q9g
The payload is encrypted simply using JWT:
Jwts.builder().setPayload(new Gson().toJson(new Person("Amit",22))).signWith(SignatureAlgorithm.HS256, key).compact();
Your request payload is not a JSON. It's a JWT token which contains a JSON encoded as Base64. It's a piece of text. Hence, the Content-Type of the request should be text/plain instead of application/json:
POST /api/auth HTTP/1.1
Host: example.org
Content-Type: text/plain
eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiQW1pdCIsImFnZSI6MjJ9.-rO6yhYJ--3ZzVCaHFw1hF-s533foYY6vVAuyRh3Q9g
Your filter modifies the payload of the request: the filter gets the JWT token from the request payload, gets the token payload, decodes the token payload into a JSON string and sets the JSON string to the request payload.
After the executing of the filter, the request will contain a JSON string and not just a piece of text. Hence, after that, the Content-Type of the request should be modified to application/json. It could be achieved with the following lines:
requestContext.getHeaders().remove(HttpHeaders.CONTENT_TYPE);
requestContext.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
To ensure the filter will be executed before the resource matching, annotate your filter with #PreMatching.
And don't forget to annotate your resource method with #Consumes(MediaType.APPLICATION_JSON).
I tried changing the payload to a JSON document as:
"temp":"eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiQW1pdCIsImFnZSI6MjJ9.-rO6yhYJ--3ZzVCaHFw1hF-s533foYY6vVAuyRh3Q9g"
and extracted the encrypted payload using the below code in my filter:
JsonObject jsonObject = new Gson().fromJson(theString, JsonObject.class);
theString = jsonObject.get("temp").getAsString();
Also changed the resource to #Consumes(MediaType.APPLICATION_JSON)
And it worked!
I guess the reason was as mentioned by pandaadb that Jersey was unable to recognize it as a JSON document and hence was unable to Map it to the Person POJO.
I am building a Spring rest service for uploading a file. There is a form that consists of various field and one field for uploading a file. On submitting that form, I am sending a multipart form request i.e. Content-Type as multipart/form-data.
So I tried with below
#RequestMapping(value = "/companies", method = RequestMethod.POST)
public void createCompany(#RequestBody CompanyDTO companyDTO, #RequestParam(value = "image", required = false) MultipartFile image){
.................
But, the above didn't work. So for time being,i sent JSON data as String and forming Company Object from that String in rest service like
#RequestMapping(value = "/companies", method = RequestMethod.POST)
public void createCompany(#RequestParam("companyJson") String companyJson, #RequestParam(value = "image",required = false) MultipartFile image) throws JsonParseException, JsonMappingException, IOException{
CompanyDTO companyDTO = new ObjectMapper().readValue(companyJson, CompanyDTO.class);
.............................
Can't I send JSON data with #RequestBody without passing JSON as String?
Appending the values to the URL what u have been doing now using #RequestParam.
#RequestParam annotation will not work for complex JSON Objects , it is specifi for Integer or String .
If it is a Http POST method , use of #RequestBody will make the Spring to map the incoming request to the POJO what u have created (condition: if the POJO maps the incoming JSON)
create FormData() and append your json and file
if (form.validate()) {
var file = $scope.file;
var fd = new FormData();
fd.append('jsondata', $scope.jsonData);
fd.append('file', file);
MyService.submitFormWithFile('doc/store.html', fd, '', (response){
console.log(response)
});
}
//Service called in above
MyService.submitFormWithFile = function(url, data, config, callback) {
$http({
method : 'POST',
url : url,
headers : {
'Content-Type' : undefined
},
data : data,
transformRequest : function(data, headersGetterFunction) {
return data;
}
}).success(function(response, status, header, config) {
if (status === 200) {
callback(response);
} else {
console.log("error")
}
}).error(function(response, status, header, config) {
console.log(response);
});
};
// in your java part using ObjectMapper
//it is like string
fd.append('jsondata', JSON.stringify($scope.jsonData));
#Autowired
private ObjectMapper mapper;
#RequestMapping(value = "/companies", method = RequestMethod.POST)
public void createCompany(#RequestParam String jsondata,
#RequestParam(required = true) MultipartFile file){
CompanyDto companyDto=mapper.readValue(jsondata, CompanyDTO.class);
......
}
Use below code snippet:
#RequestMapping(value= "/path", method=RequestMethod.POST, produces=MediaType.APPLICATION_JSON_VALUE)
public ResponseObject methodName(MyData input, #RequestParam(required=false) MultipartFile file) {
// To Do
}
I'm trying to accomplish a multipart file upload using feign, but I can't seem to find a good example of it anywhere. I essentially want the HTTP request to turn out similar to this:
...
Content-Type: multipart/form-data; boundary=AaB03x
--AaB03x
Content-Disposition: form-data; name="name"
Larry
--AaB03x
Content-Disposition: form-data; name="file"; filename="file1.txt"
Content-Type: text/plain
... contents of file1.txt ...
--AaB03x--
Or even...
------fGsKo01aQ1qXn2C
Content-Disposition: form-data; name="file"; filename="file.doc"
Content-Type: application/octet-stream
... binary data ...
------fGsKo01aQ1qXn2C--
Do I need to manually build the request body, including generating the multipart boundaries? That seems a bit excessive considering everything else this client can do.
No, you don't. You just need to define a kind of proxy interface method, specify the content-type as: multipart/form-data and other info such as parameters required by the remote API. Here is an example:
public interface FileUploadResource {
#RequestLine("POST /upload")
#Headers("Content-Type: multipart/form-data")
Response uploadFile(#Param("name") String name, #Param("file") File file);
}
The completed example can be found here: File Uploading with Open Feign
For spring boot 2 and spring-cloud-starter-openfeign use this code:
#PostMapping(value="/upload", consumes = "multipart/form-data" )
QtiPackageBasicInfo upload(#RequestPart("package") MultipartFile package);
You need to change #RequestParam to #RequestPart in the feign client call to make it work, and also add consumes to the #PostMapping.
MBozic solution not full, you will also need to enable an Encoder for this:
public class FeignConfig {
#Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
#Bean
public Encoder feignFormEncoder () {
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
}
#FeignClient(name = "file", url = "http://localhost:8080", configuration = FeignConfig.class)
public interface UploadClient {
#PostMapping(value = "/upload-file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String fileUpload(#RequestPart(value = "file") MultipartFile file);
}
If you are already using Spring Web, you can try my implementation of a Feign Encoder that is able to create Multipart requests. It can send a single file, an array of files alongwith one or more additional JSON payloads.
Here is my test project. If you don't use Spring, you can refactor the code by changing the encodeRequest method in FeignSpringFormEncoder.
Let me add Answer for latest OpenFeign :
Add dependency for Feign-Form:
io.github.openfeign.form
feign-form
3.8.0
Add FormEncoder to your Feign.Builder like so:
SomeApi github = Feign.builder()
.encoder(new FormEncoder())
.target(SomeApi.class, "http://api.some.org");
API endpoint
#RequestLine("POST /send_photo")
#Headers("Content-Type: multipart/form-data")
void sendPhoto (#Param("is_public") Boolean isPublic, #Param("photo") FormData photo);
Refer : https://github.com/OpenFeign/feign-form
Call from one service to another service for file transfer/upload/send using feign client interface:
#FeignClient(name = "service-name", url = "${service.url}", configuration = FeignTokenForwarderConfiguration.class)
public interface UploadFeignClient {
#PostMapping(value = "upload", headers = "Content-Type= multipart/form-data", consumes = "multipart/form-data")
public void upload(#RequestPart MultipartFile file) throws IOException;
}
**Actual API:**
#RestController
#RequestMapping("upload")
public class UploadController {
#PostMapping(value = "/upload", consumes = { "multipart/form-data" })
public void upload(#RequestParam MultipartFile file) throws IOException {
//implementation
}
}
I have a REST endpoint #POST where the form params are null when the Content-Type is application/x-www-form-urlencoded. There is a ContainerRequestFilter earlier in the chain (code at the bottom) that takes the request, changes the stream to a BufferedInputStream, and then logs the request. If I remove this logging code, the endpoint has the correct form params. Otherwise, they're null and I can't figure out why.
Now if I use application/json, my endpoint has the correct params regardless if the logger is enabled or disabled.
I need application/x-www-form-urlencoded because the REST endpoint needs to redirect and browsers prevent redirection if the request isn't standard (preflight)
REST Endpoint that isn't working (OAuthRequest has null members)
#Stateless
#Path("v1/oauth2")
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
#Produces(MediaType.APPLICATION_JSON)
public class OAuthTokenResource {
#POST
public Response getToken(#Form OAuthRequest oauthRequest) {
...
}
OAuthRequest
public class OAuthRequest {
#FormParam(OAuthParam.CLIENT_ID)
#JsonProperty(OAuthParam.CLIENT_ID)
private String clientId;
#URL
#FormParam(OAuthParam.REDIRECT_URI)
#JsonProperty(OAuthParam.REDIRECT_URI)
private String redirectUri;
#FormParam(OAuthParam.USERNAME)
private String username;
#FormParam(OAuthParam.PASSWORD)
private String password;
...
}
Logging Filter
#Override
public void filter(final ContainerRequestContext context) throws IOException {
...
if (logEntity && context.hasEntity()) {
context.setEntityStream(logInboundEntity(builder, context.getEntityStream(), context.getMediaType()));
}
logger.debug(builder.toString());
}
private InputStream logInboundEntity(final StringBuilder builder, InputStream stream, MediaType mediaType) throws IOException {
if (!stream.markSupported()) {
stream = new BufferedInputStream(stream);
}
stream.mark(maxEntitySize + 1);
final byte[] entity = new byte[maxEntitySize + 1];
final int entitySize = stream.read(entity);
if ( entitySize > 0 ) {
String body = new String(entity, 0, Math.min(entitySize, maxEntitySize), StandardCharsets.UTF_8);
builder.append("\nBody: ");
builder.append(body);
}
if (entitySize > maxEntitySize) {
builder.append(MORE_INDICATOR);
}
stream.reset();
return stream;
}
Okay I am still not sure why #Form and #FormParam does not work if the InputStream is read during the filter chain.
But, I discovered a workaround as follows.
#POST
public Response getToken(MultivaluedMap<String, String> formParams) {
...
}
This provides the same behavior as during application/json as the params are already set even if the InputStream has been consumed.
However ultimately we went with disabling logging of the request body in our filter for security reasons.