[SWAGGER 2 UI]: Enabling multipart / form-data requests - spring-boot

I have a simple controller method that captures some JSON information as well as an arbitrary number of uploaded image files:
#PostMapping(value = "/products", consumes = MediaType.MULTIPART_FORM_DATA_VALUE )
public ResponseEntity<ResponseMessage> postProduct(#RequestPart(name = "request") final MyJsonBody postRequest,
#RequestPart(name = "images") final ist<MultipartFile> images)
{
log.info("Request id and name fields: " + postRequest.getProductId() + ", " + postRequest.getProductName() + ".");
log.info("Received a total of: " + images.size() + " files.");
return success("Just wanted to test the upload functionality!", null, HttpStatus.OK); // A private method I have to return an appropriate ResponseEntity<> to the user.
}
The MyJsonBody class is a simple POJO for testing purposes:
#Data
#Builder(access = PUBLIC)
#AllArgsConstructor
#NoArgsConstructor
public class MyJsonBody
{
#JsonProperty("id") private String productId;
#JsonProperty("name") private String productName;
}
When using Postman, my example multipart/form-data POST request works just fine:
As you can see here, Springboot is completely fine with the request and prints the data as expected:
2020-12-21 14:28:20.321 INFO 11176 --- [nio-8080-exec-3] c.j.r.fileuploads.FileUploadController : Request id and name fields: RANDOM_ID, null.
2020-12-21 14:28:20.321 INFO 11176 --- [nio-8080-exec-3] c.j.r.fileuploads.FileUploadController : Received a total of: 2 files.
My team uses Swagger 2, and it's important to me to be able to debug the API using the Swagger UI. Unfortunately, when sending an identical request from the UI:
The UI appears to not be sending the request as a multipart/form-data request, but as an application/octet-stream request:
2020-12-21 14:27:40.095 WARN 11176 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/octet-stream' not supported]
Most information on the Web concerns the OpenAPI spec version 3, yet we are using version 2.8 for now.
Any ideas about how I might be malforming the request?
Edit: The following is my configuration class for Swagger. All of my code in this toy projects is under package com.jason.rest.fileuploads.
#Configuration
#EnableSwagger2
public class SwaggerConfig extends WebMvcConfigurationSupport {
#Bean
public Docket productApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select().apis(RequestHandlerSelectors.basePackage("com.jason.rest.fileuploads"))
.paths(PathSelectors.any())
.build();
}
#Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
#Bean
public LinkDiscoverers discoverers() {
List<LinkDiscoverer> plugins = new ArrayList<>();
plugins.add(new CollectionJsonLinkDiscoverer());
return new LinkDiscoverers(SimplePluginRegistry.create(plugins));
}
}

Related

Spring Boot WebFlux with RouterFunction log request and response body

I'm trying to log request and response body of every call received by my microservice which is using reactive WebFlux with routing function defined as bellow:
#Configuration
public class FluxRouter {
#Autowired
LoggingHandlerFilterFunction loggingHandlerFilterFunction;
#Bean
public RouterFunction<ServerResponse> routes(FluxHandler fluxHandler) {
return RouterFunctions
.route(POST("/post-flux").and(accept(MediaType.APPLICATION_JSON)), fluxHandler::testFlux)
.filter(loggingHandlerFilterFunction);
}
}
#Component
public class FluxHandler {
private static final Logger logger = LoggerFactory.getLogger(FluxHandler.class);
#LogExecutionTime(ActionType.APP_LOGIC)
public Mono<ServerResponse> testFlux(ServerRequest request) {
logger.info("FluxHandler.testFlux");
return request
.bodyToMono(TestBody.class)
.doOnNext(body -> logger.info("FluxHandler-2: "+ body.getName()))
.flatMap(testBody -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue("Hello, ")));
}
}
and a router filter defined like this:
#Component
public class LoggingHandlerFilterFunction implements HandlerFilterFunction<ServerResponse, ServerResponse> {
private static final Logger logger = LoggerFactory.getLogger(LoggingHandlerFilterFunction.class);
#Override
public Mono<ServerResponse> filter(ServerRequest request, HandlerFunction<ServerResponse> handlerFunction) {
logger.info("Inside LoggingHandlerFilterFunction");
return request.bodyToMono(Object.class)
.doOnNext(o -> logger.info("Request body: "+ o.toString()))
.flatMap(testBody -> handlerFunction.handle(request))
.doOnNext(serverResponse -> logger.info("Response body: "+ serverResponse.toString()));
}
}
Console logs here are:
Inside LoggingHandlerFilterFunction
LoggingHandlerFilterFunction-1: {name=Name, surname=Surname}
FluxHandler.testFlux
As you can see I'm able to log the request but after doing it the body inside the FluxHandler seems empty because this is not triggered .doOnNext(body -> logger.info("FluxHandler-2: "+ body.getName()) and also no response body is produced. Can someone show me how to fix this or is there any other way to log request/response body when using routes in webflux?

Spring cloud open Feign Not ignoring null Values while Encoding

I am working on a Spring boot application. We are using Spring cloud open Feign for making rest calls. We are using the default GsonEncoder(), but for some reason gson is not excluding the null properties while encoding the payload.
Config:
return Feign.builder()
.options(ApiOptions())
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.target(ApiClient.class, "URL");
Client:
#FunctionalInterface
#FeignClient(
value = "apiTest",
url = "urlHere"
)
public interface ApiClient {
#PostMapping("path/to/service")
AiResponse getDetails(ApiRequest apiRequest);
}
ApiRequest.java:
public class ApiRequest {
private String userName;
private String userId;
private String password;
//#setters and getters
}
But while making the request, the Request Body is :
{
"userName" : "test,
"userId" : null,
"password": "password"
}
My Understanding is that Gson should automatically remove the null while serializing the Request Body. But i can see null properties exist in request.
I even tried with Custom Encoders (Jackson from below):
https://github.com/OpenFeign/feign/blob/master/jackson/src/main/java/feign/jackson/JacksonEncoder.java
As per below, it should not include null while serialzing the requestBody, but still i can see null values being passed in request.
https://github.com/OpenFeign/feign/blob/master/jackson/src/main/java/feign/jackson/JacksonEncoder.java#L39
Below are the dependencies:
Spring clou version : 2020.0.2
org.springframework.cloud:spring-cloud-starter-openfeign
io.github.openfeign:feign-gson:9.5.1
Any suggestions would be really helpful. Thanks in Advance.!
You need to provide OpenFeign with custom Encoder to let it skip nulls.
In my case this beans helps me.
#Configuration
public class ExternalApiConfiguration {
#Bean
public Feign.Builder feignBuilder() {
return Feign.builder()
.contract(new SpringMvcContract());
}
#Bean("feignObjectMapper")
public ObjectMapper feignObjectMapper() {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return objectMapper;
}
#Bean
public Encoder feignEncoder(#Qualifier("feignObjectMapper") ObjectMapper objectMapper) {
HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(objectMapper);
ObjectFactory<HttpMessageConverters> objectFactory = () -> new HttpMessageConverters(jacksonConverter);
return new SpringEncoder(objectFactory);
}
And use this configuration if FeignClient
#FeignClient(configuration = ExternalApiConfiguration.class)

How to reinvoke WebClient's ExchangeFilterFunction on retry

When using reactor's retry(..) operator WebClient exchange filter functions are not triggered on retry. I understand why, but the issue is when a function (like bellow) generates an authentication token with an expiry time. It might happen, while a request is being "retried" the token expires because the Exchange function is not re-invoked during the retry. Is there a way how to re-generate a token for each retry?
Following AuthClientExchangeFunction generates an authentication token (JWT) with an expiration.
public class AuthClientExchangeFunction implements ExchangeFilterFunction {
private final TokenProvider tokenProvider;
public IntraAuthWebClientExchangeFunction(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
String jwt = tokenProvider.getToken();
return next.exchange(withBearer(request, jwt));
}
private ClientRequest withBearer(ClientRequest request, String jwt){
return ClientRequest.from(request)
.headers(headers -> headers.set(HttpHeaders.AUTHORIZATION, "Bearer "+ jwt))
.build();
}
}
Lets say that a token is valid for 2999 ms -> Each retry request fails due to 401.
WebClient client = WebClient.builder()
.filter(new AuthClientExchangeFunction(tokenProvider))
.build();
client.get()
.uri("/api")
.retrieve()
.bodyToMono(String.class)
.retryBackoff(1, Duration.ofMillis(3000)) ;
Edit
Here is an executable example
#SpringBootTest
#RunWith(SpringRunner.class)
public class RetryApplicationTests {
private static final MockWebServer server = new MockWebServer();
private final RquestCountingFilterFunction requestCounter = new RquestCountingFilterFunction();
#AfterClass
public static void shutdown() throws IOException {
server.shutdown();
}
#Test
public void test() {
server.enqueue(new MockResponse().setResponseCode(500).setBody("{}"));
server.enqueue(new MockResponse().setResponseCode(500).setBody("{}"));
server.enqueue(new MockResponse().setResponseCode(500).setBody("{}"));
server.enqueue(new MockResponse().setResponseCode(200).setBody("{}"));
WebClient webClient = WebClient.builder()
.baseUrl(server.url("/api").toString())
.filter(requestCounter)
.build();
Mono<String> responseMono1 = webClient.get()
.uri("/api")
.retrieve()
.bodyToMono(String.class)
.retryBackoff(3, Duration.ofMillis(1000)) ;
StepVerifier.create(responseMono1).expectNextCount(1).verifyComplete();
assertThat(requestCounter.count()).isEqualTo(4);
}
static class RquestCountingFilterFunction implements ExchangeFilterFunction {
final Logger log = LoggerFactory.getLogger(getClass());
final AtomicInteger counter = new AtomicInteger();
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
log.info("Sending {} request to {} {}", counter.incrementAndGet(), request.method(), request.url());
return next.exchange(request);
}
int count() {
return counter.get();
}
}
}
output
MockWebServer[44855] starting to accept connections
Sending 1 request to GET http://localhost:44855/api/api
MockWebServer[44855] received request: GET /api/api HTTP/1.1 and responded: HTTP/1.1 500 Server Error
MockWebServer[44855] received request: GET /api/api HTTP/1.1 and responded: HTTP/1.1 500 Server Error
MockWebServer[44855] received request: GET /api/api HTTP/1.1 and responded: HTTP/1.1 500 Server Error
MockWebServer[44855] received request: GET /api/api HTTP/1.1 and responded: HTTP/1.1 200 OK
org.junit.ComparisonFailure:
Expected :4
Actual :1
You need to update your spring-boot version to 2.2.0.RELEASE. retry() will not invoke exchange function in previous version.
I've tested this using a simple code (In Kotlin).
#Component
class AnswerPub {
val webClient = WebClient.builder()
.filter(PrintExchangeFunction())
.baseUrl("https://jsonplaceholder.typicode.com").build()
fun productInfo(): Mono<User> {
return webClient
.get()
.uri("/todos2/1")
.retrieve()
.bodyToMono(User::class.java)
.retry(2) { it is Exception }
}
data class User(
val id: String,
val userId: String,
val title: String,
val completed: Boolean
)
}
class PrintExchangeFunction : ExchangeFilterFunction {
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
println("Filtered")
return next.exchange(request)
}
}
And the console output looked like:
2019-10-29 09:31:55.912 INFO 12206 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2019-10-29 09:31:55.917 INFO 12206 --- [ main] c.e.s.SpringWfDemoApplicationKt : Started SpringWfDemoApplicationKt in 3.19 seconds (JVM running for 4.234)
Filtered
Filtered
Filtered
So in my case, the exchange function is invoked every single time.

Spring boot + Swagger UI how to tell endpoint to require bearer token

I'm using Spring Boot to build a REST API. I've added Swagger-ui to handle documentation. I'm having a problem implementation the client authentication flow into swagger, the problem being I can get swagger-ui to authorise a supplied client-id(username) and client-secret(password) via basic auth, but swagger UI doesn't appear to be then applying to resulting access token to endpoint calls.
To confirm, my authorisation process;
- Use basic auth to send base64 encoded username/password & grant_type=client_credentials to /oauth/token. Spring returns an access_token
- On future API calls, use the supplied access_token as the bearer token
I think that the problem may be because I need to place something on each method in my controllers to tell swagger that the endpoint requires authentication and what type, but I can't find any clear documentation on how to do this, and I don't know if I need to apply any further changes to my swagger config.
Here's an example of a controller (with most methods removed to reduce size);
#Api(value="Currencies", description="Retrieve, create, update and delete currencies", tags = "Currencies")
#RestController
#RequestMapping("/currency")
public class CurrencyController {
private CurrencyService currencyService;
public CurrencyController(#Autowired CurrencyService currencyService) {
this.currencyService = currencyService;
}
/**
* Deletes the requested currency
* #param currencyId the Id of the currency to delete
* #return 200 OK if delete successful
*/
#ApiOperation(value = "Deletes a currency item", response = ResponseEntity.class)
#RequestMapping(value="/{currencyId}", method=RequestMethod.DELETE)
public ResponseEntity<?> deleteCurrency(#PathVariable("currencyId") Long currencyId) {
try {
currencyService.deleteCurrencyById(currencyId);
} catch (EntityNotFoundException e) {
return new ErrorResponse("Unable to delete, currency with Id " + currencyId + " not found!").response(HttpStatus.NOT_FOUND);
}
return new ResponseEntity(HttpStatus.OK);
}
/**
* Returns a single currency by it's Id
* #param currencyId the currency Id to return
* #return the found currency item or an error
*/
#ApiOperation(value = "Returns a currency item", response = CurrencyResponse.class)
#RequestMapping(value="/{currencyId}", method = RequestMethod.GET, produces = "application/json")
public ResponseEntity<RestResponse> getCurrency(#PathVariable("currencyId") Long currencyId) {
Currency currency = null;
try {
currency = currencyService.findById(currencyId);
} catch (EntityNotFoundException e) {
return new ErrorResponse("Currency with Id " + currencyId + " could not be found!").response(HttpStatus.NOT_FOUND);
}
return new CurrencyResponse(currency).response(HttpStatus.OK);
}
/**
* Returns a list of all currencies available in the system
* #return Rest response of all currencies
*/
#ApiOperation(value = "Returns a list of all currencies ordered by priority", response = CurrencyListResponse.class)
#RequestMapping(value="", method=RequestMethod.GET, produces="application/json")
public ResponseEntity<RestResponse> getCurrencies() {
return new CurrencyListResponse(currencyService.getAllCurrencies()).response(HttpStatus.OK);
}
}
Here is my current swagger config;
#Configuration
#EnableSwagger2
public class SwaggerConfig extends WebMvcConfigurationSupport {
#Bean
public SecurityConfiguration security() {
return SecurityConfigurationBuilder.builder()
.clientId("12345")
.clientSecret("12345")
.scopeSeparator(" ")
.useBasicAuthenticationWithAccessCodeGrant(true)
.build();
}
#Bean
public Docket productApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.xompare.moo.controllers"))
.build()
.securitySchemes(Arrays.asList(securityScheme()))
.securityContexts(Arrays.asList(securityContext()))
.apiInfo(metaData());
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(Arrays.asList(new SecurityReference("spring_oauth", scopes())))
.forPaths(PathSelectors.regex("/.*"))
.build();
}
private AuthorizationScope[] scopes() {
AuthorizationScope[] scopes = {
new AuthorizationScope("read", "for read operations"),
new AuthorizationScope("write", "for write operations") };
return scopes;
}
public SecurityScheme securityScheme() {
GrantType grantType = new ClientCredentialsGrant("http://localhost:8080/oauth/token");
SecurityScheme oauth = new OAuthBuilder().name("spring_oauth")
.grantTypes(Arrays.asList(grantType))
.scopes(Arrays.asList(scopes()))
.build();
return oauth;
}
#Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
Authentication via spring works perfectly at this point, my only problem is getting it working with Swagger UI.
I think that you need to add "Bearer " in front of your key, just like it is shown at this post:
Spring Boot & Swagger UI. Set JWT token
I managed to resolve this by reverting from swagger-ui version 2.8.0 to 2.7.0 after reading the contents of this link which suggested it was a problem with version 2.8.0
https://github.com/springfox/springfox/issues/1961

Feign recognized GET method as POST

I have a service defined as follow
class Echo {
private String message; // getters and setters omitted
}
#RequestMapping("/app")
interface Resource {
#RequestMapping(method = GET)
Echo echo(#ModelAttribute Echo msg);
}
#RestController
class ResourceImpl implements Resource {
#Override
Echo echo(Echo msg) { return msg; }
}
and his client on a different application
#FeignClient(name = "app", url = "http://localhost:8080")
interface Client extends Resource {}
However, when I call resource method
#Autowired
private Resource client;
#Test
public void test() {
Echo echo = new Echo();
echo.setMessage("hello");
client.echo(echo);
}
I got a confusing error message
feign.FeignException: status 405 reading ClientLocal#echo(Echo);
content: {"timestamp":1512686240485,"status":405,"error":"Method Not
Allowed","exception":"org.springframework.web.HttpRequestMethodNotSupportedException","message":"Request
method 'POST' not supported","path":"/app"}
What I did wrong here?
Found the same issue and for me the reason of POST with GET mixing by Feign is usage of Object as request param
was with same error as yours:
#GetMapping("/followers/{userId}/id")
Page<String> getFollowersIds(#PathVariable String userId, Pageable pageable);
added #RequstParam for 2 argument fixed it, like:
#GetMapping("/followers/{userId}/id")
Page<String> getFollowersIds(#PathVariable String userId, #RequestParam Pageable pageable);

Resources