How can I log "real" Http Headers on WebClient - spring

I'm currently using ExchangeFilterFunction to log all Headers that come inside the ClientRequest instance and I'm accessing to them doing request.headers().
After my filter is executed, the HttpClient underneath is adding certain headers such as the Accept-Encoding one, thus not getting logged, as they never get added to the ClientRequest instance.
My filter looks like this:
public class WebClientLoggingFilter implements ExchangeFilterFunction {
#Override
public Mono<ClientResponse> filter(final ClientRequest clientRequest, final ExchangeFunction next) {
return Mono.just(clientRequest)
.doOnNext(request -> log(request.headers().toString()))
.flatMap(next::exchange)
.doOnNext(clientResponse -> logData(clientRequestData, message, clientResponse));
}
}
This filter logs everything inside ClientRequest headers, but then HttpClient does its magic, which never arrives at the ClientRequest, even after the response is back. Example of code from Netty.
Is there any other way I can do the logging so I can get access to what it's truly being sent through the network?

Instead of using a filter, I'd recommend utilising the standard loggers by adding these lines to your resources/application.properties:
spring.http.log-request-details=true
logging.level.org.springframework.web.reactive.function.client.ExchangeFunctions=TRACE
However, by default, this will still show headers as {headers masked} (as they may contain sensitive data). To enable header logging for a client, you must explicitly enable it on each WebClient as follows:
return WebClient
.builder()
.exchangeStrategies(ExchangeStrategies.builder().codecs(c ->
c.defaultCodecs().enableLoggingRequestDetails(true)).build()
)
.build()
//carry on using the webclient as normal
You'll then get output similar to the following:
HTTP POST https://a.com/ea, headers=[Content-Type:"application/json", Accept:"application/json", Authorization:"Bearer token blah"]

The configuration property to use has changed to:
spring.codec.log-request-details=true

Related

Spring Webflux: how to manually write headers and body?

I'm using Spring WebFlux for my project that is intended to work as pub/sub service: http clients connect to it and wait for events (like PUSH or SSE).
I need to manually write headers and body to the response without using ServerResponse.
I need to do it manually because I'm implementing an SSE server and I must send custom headers into the response before any event actually arrives.
I'm trying to do it this way:
#Bean
RouterFunction<ServerResponse> getStuff() {
return route(GET("/stuff"),
request -> {
final ServerWebExchange exchange = request.exchange();
final byte[] bytes = "data".getBytes(StandardCharsets.UTF_8);
final DataBuffer buffer =exchange.getResponse().bufferFactory().wrap(bytes);
return exchange.getResponse().writeWith(Flux.just(buffer));
}
);
But it does not work because writeWith() returns Mono<Void> and getStuff() must return RouterFunction. Can anybody help me find a way around this?

How do I get the HTTP status code of a given URL via Spring?

I am working in a Spring #Component class and I am trying to get the HTTP status code of a particular URL for further processing. I have a function as follows:
fun getStatus() : String
{
val webClient = WebClient.create("https://stackoverflow.com")
val result = webClient.get()
.exchange().map { res -> res.rawStatusCode() }
println(result)
return "statusGotten"
}
However, rather than getting the Int value of the status code (e.g. 200, or 401), I am simply getting: "MonoMap".
I am new to both Spring and Web Programming in general, so I'm a little confused how to proceed from here. I'm aware that "result" is being returned as a "Mono", but I'm less clear about what a "Mono" is, or how I might transform it into something with more scrutable properties, as even looking at "result" in the debugger doesn't shed any light as to whether the HTTP request was actually sent or was successful:
Am I calling the webclient incorrectly? Or merely failing to parse the resultant data in a meaningful way? Any suggestions on how or where I might learn more about the underlying topics would be much appreciated as well.
If you need a blocking way to do this is easy just
#Test
public void myTest(){
WebClient client = WebClient.builder().baseUrl("https://stackoverflow.com/").build();
ClientResponse resp = client
.get()
.uri("questions/")
.exchange()
.block();
System.out.println("Status code response is: "+resp.statusCode());
}
But for this you can use directly the RestTemplate instead the webclient... the recomended way to do this is non blocking what means you should return a Mono with the status and consume outside your method like for example:
public Mono<HttpStatus> myMethod(){
WebClient client = WebClient.builder().baseUrl("https://stackoverflow.com/").build();
return client
.get()
.uri("questions/")
.exchange()
.map( clientResp -> clientResp.statusCode());
}
The way of consume this Mono depends of your code...

How to redirect request to an URI passed in headers with Jetty AsyncProxyServlet

I'm creating a proxy micro-service with SpringBoot, Jetty and kotlin.
The purpose of this micro-service is to forward requests made by my front-end application to external services (avoiding CORS) and send back the response after checking some custom authentication. The query I'll receive will contain the URL of the target in the headers (i.e: Target-Url: http://domain.api/getmodel).
Based on this answer, I made a class that extends AsyncProxyServlet and overwrote the method sendProxyRequest :
class ProxyServlet : AsyncProxyServlet() {
private companion object {
const val TARGET_URL = "Target-Url"
}
override fun sendProxyRequest(clientRequest: HttpServletRequest, proxyResponse: HttpServletResponse, proxyRequest: Request) {
// authentication logic
val targetUrl = clientRequest.getHeader(TARGET_URL)
if (authSuccess) {
super.sendProxyRequest(clientRequest, proxyResponse, proxyRequest)
} else {
proxyResponse.status = HttpStatus.UNAUTHORIZED.value()
}
}
}
When I query my proxy, I get in this method and successfuly authenticate, but I fail to understand how to use my targetUrl to redirect the request.
The method keeps calling itself as it's redirecting the original request to itself (the request from http://myproxy:port/ to http://myproxy:port/).
It is very difficult to find documentation on this specific implementation of jetty, StackOverflow is my last resort!
First, setup logging for Jetty, and configure DEBUG level logging for the package namespace org.eclipse.jetty.proxy, this will help you understand the behavior much better.
The Request proxyRequest parameter represents a HttpClient/Request object, which is created with an immutable URI/URL destination (this is due to various other features that requires information from the URI/URL such as Connection pooling, Cookies, Authentication, etc), you cannot change the URI/URL on this object after the fact, you must create the HttpClient/Request object with the correct URI/URL.
Since all you want to do is change the target URL, you should instead be overriding the method ...
protected String rewriteTarget(HttpServletRequest clientRequest)
... and returning the new absolute URI String to the destination that you want to use (The "Target-Url" header in your scenario looks like a good candidate)
You can see this logic in the ProxyServlet.service(HttpServletRequest, HttpServletResponse) code block (which AsyncProxyServlet extends from)

Missing Content-Length header sending POST request with WebClient (SpringBoot 2.0.2.RELEASE)

I'm using WebClient (SpringBoot 2.0.2.RELEASE) to send a POST with SOAP request, but it is missing "Content-Length" header required by the legacy API.
Is it possible to configure WebClient to include "Content-Length" header?
There is an Spring Framework Issue resolved and introduced for EncoderHttpMessageWriter in SpringBoot 2.0.1, but it seems not to work for JAXB.
I tried to use BodyInserters:
webClient.post().body(BodyInserters.fromObject(request)).exchange();
and syncBody:
webClient.post().syncBody(request).exchange();
None of them worked for WebClient. Though, when RestTemplate is used, Content-Length is set and API responds with success
I am struggling with the same problem, as an ugly work-around I am manually serializing the request (JSON in my case) and setting the length (Kotlin code):
open class PostRetrieverWith411ErrorFix(
private val objectMapper: ObjectMapper
) {
protected fun <T : Any> post(webClient: WebClient, body: Any, responseClass: Class<T>): Mono<T> {
val bodyJson = objectMapper.writeValueAsString(body)
return webClient.post()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.contentLength(bodyJson.toByteArray(Charset.forName("UTF-8")).size.toLong())
.syncBody(bodyJson)
.retrieve()
.bodyToMono(responseClass)
}
}
If you apply Sven's colleague(Max) solution like we did you can also adapt it for cases like your body being a custom obj but you have to serialize it once:
String req = objectMapper.writeValueAsString(requestObject)
and passed that to
webClient.syncBody(req)
Keep in mind that with SpringBoot 2.0.3.RELEASE, if you'll pass a String to webClient as a request, it will put as ContentType header MediaType.TEXT_PLAIN and that made our integration with other service to fail. We fixed that by setting specifically content type header like this:
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
WebClient is a streaming client and it's kind of difficult to set the content length until the stream has finished. By then the headers are long gone. If you work with legacy, you can re-use your mono (Mono/Flux can be reused, Java streams not) and check the length.
public void post() {
Mono<String> mono = Mono.just("HELLO WORLDZ");
final String response = WebClient.create("http://httpbin.org")
.post()
.uri("/post")
.header(HttpHeaders.CONTENT_LENGTH,
mono.map(s -> String.valueOf(s.getBytes(StandardCharsets.UTF_8).length)).block())
.body(BodyInserters.fromPublisher(mono, String.class))
.retrieve()
.bodyToMono(String.class)
.block();
System.out.println(response);
}
A colleague (well done Max!) of mine came up with cleaner solution, I added some wrapping code so it can be tested:
Mono<String> my = Mono.just("HELLO WORLDZZ")
.flatMap(body -> WebClient.create("http://httpbin.org")
.post()
.uri("/post")
.header(HttpHeaders.CONTENT_LENGTH,
String.valueOf(body.getBytes(StandardCharsets.UTF_8).length))
.syncBody(body)
.retrieve()
.bodyToMono(String.class));
System.out.println(my.block());

How to make a #RestController POST method ignore Content-Type header and only use request body?

I'm using latest Spring Boot (1.2.1) and whatever Spring MVC version comes with it.
I have a controller method with implicit JSON conversions for both incoming and outgoing data:
#RestController
public class LoginController {
#RequestMapping(value = "/login", method = POST, produces = "application/json")
ResponseEntity<LoginResponse> login(#RequestBody LoginRequest loginRequest) {
// ...
}
}
This works fine, but only if request Content-Type is set to application/json. In all other cases, it responds with 415, regardless of the request body:
{
"timestamp": 1423844498998,
"status": 415,
"error": "Unsupported Media Type",
"exception": "org.springframework.web.HttpMediaTypeNotSupportedException",
"message": "Content type 'text/plain;charset=UTF-8' not supported",
"path": "/login/"
}
Thing is, I'd like to make my API more lenient; I want Spring to only use the POST request body and completely ignore Content-Type header. (If request body is not valid JSON or cannot be parsed into LoginRequest instance, Spring already responds with 400 Bad Request which is fine.) Is this possible while continuing to use the implicit JSON conversions (via Jackson)?
I've tried consumes="*", and other variants like consumes = {"text/*", "application/*"} but it has no effect: the API keeps giving 415 if Content-Type is not JSON.
Edit
It looks like this behaviour is caused by MappingJackson2HttpMessageConverter whose documentation says:
By default, this converter supports application/json and
application/*+json. This can be overridden by setting the supportedMediaTypes property.
I'm still missing how exactly do I customise that, for example in a
custom Jackson2ObjectMapperBuilder...
I assume that you are using default MappingJackson2HttpMessageConverter provided by Spring.
If you would like to have the same behavior in all requests, one solution would be to write custom converter which will not look for Content-Type, in a header (instead will parse to JSON alwayse) and then configure Spring to use your custom one. Again this will affect all requests, so might not fit all needs.
public class CustomerJsonHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
private ObjectMapper mapper = new ObjectMapper();
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
public CustomerJsonHttpMessageConverter() {
super(new MediaType("application", "json", DEFAULT_CHARSET));
}
#Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
return mapper.readValue(inputMessage.getBody(), clazz);
}
#Override
protected boolean supports(Class<?> clazz) {
return true;
}
#Override
protected void writeInternal(Object value, HttpOutputMessage outputMessage) throws IOException,
HttpMessageNotWritableException {
String json = mapper.writeValueAsString(value);
outputMessage.getBody().write(json.getBytes());
}
}
To have custom media type,
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(
Arrays.asList(
new MediaType("text", "plain"),
new MediaType("text", "html")
));
For anyone else who is curious about this;
It is possible to customize the used MappingJackson2HttpMessageConverter by overridding WebMvcConfigurerAdapter.extendMessageConverters to allow for multiple mime types.
However, it does not work as expected because application/x-www-form-urlencoded is hardcoded in ServletServerHttpRequest.getBody to modify the body to be url encoded (even if the post data is JSON) before passing it to MappingJackson2HttpMessageConverter.
If you really needed this to work then I think the only way is to put a Filter that modifies the request content-type header before handling (not to imply this is a good idea, just if the situation arises where this is necessary).
Update: watch out if you use this
(This was probably a stupid idea anyway.)
This has the side effect that server sets response Content-Type to whatever the first value in the request's Accept header is! (E.g. text/plain instead of the correct application/json.)
After noticing that, I got rid of this customisation and settled went with Spring's default behaviour (respond with 415 error if request does not have correct Content-Type).
Original answer:
MappingJackson2HttpMessageConverter javadocs state that:
By default, this converter supports application/json and application/*+json. This can be overridden by setting the supportedMediaTypes property.
...which pointed me towards a pretty simple solution that seems to work. In main Application class:
#Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter =
new MappingJackson2HttpMessageConverter(new CustomObjectMapper());
converter.setSupportedMediaTypes(Arrays.asList(MediaType.ALL));
return converter;
}
(CustomObjectMapper is related to other Jackson customisations I have; that contructor parameter is optional.)
This does affect all requests, but so far I don't see a problem with that in my app. If this became a problem, I'd probably just switch the #RequestBody parameter into String, and deserialise it manually.

Resources