I've created a route to allow me to forward a REST call. Everything is going well, except I cannot modify the HTTP headers of the response (actually I can't even get them untouched on the response).
// My processor
private void remplacerLiensDansHeader(final Exchange exchange, final String rootUrlPivotJoram, final String rootUrlRemplacement) {
// That is OK, I get the values just fine
ArrayList<String> oldLinks = exchange.getIn().getHeader(HEADER_LINK, ArrayList.class);
// This is also OK
final List<String> newLinks = anciensLiens.stream().map(lien -> lien.replace(rootUrlPivotJoram, rootUrlRemplacement)).collect(toList());
// No error, but apparently doesn't work
exchange.getMessage().setHeader(HEADER_LINK, newLinks);
exchange.getMessage().setHeader("test", "test");
}
// Route configuration
#Override
public void configure() throws Exception {
this.from(RestRouteBuilder.endPoint(createProducerJoramConfiguration))
.setExchangePattern(InOut)
.removeHeader(Exchange.HTTP_URI)
.toD(createProducerJoramConfiguration.getUrlDestination())
.setHeader("test", () -> "test") // that doesn't work either
.process(createProducerJoramConfiguration.getProcessor()); // this is the processor with the code above
}
This is the response I get (note that the response code is 200 and I think it's weird as the original is 201)
curl -v -XPost --user "xxx:yyyy" http://localhost:10015/zzzz/webservices/xxxxx
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 10015 (#0)
* Server auth using Basic with user 'xxx'
> Post /zzzzz/webservices/eeee HTTP/1.1
> Host: localhost:10015
> Authorization: Basic pppppppppppppppppp==
> User-Agent: curl/7.55.1
> Accept: */*
>
< HTTP/1.1 200
< Date: Tue, 31 Aug 2021 11:17:49 GMT
< Content-Type: application/octet-stream
< Content-Length: 0
<
* Connection #0 to host localhost left intact
Two things I've noticed:
if I add a body in the processor, then the body is present in the response,
if I remove the processor, the headers present in the "original response" are not present either.
I don't know what headers you exactly lose, but be aware that the Camel HTTP component has a default header filter (as lots of Camel components have).
If you don't specify your own HeaderFilterStrategy, the default HttpHeaderFilterStrategy is used.
This default filters the following headers:
content-length
content-type
host
cache-control
connection
date
pragma
trailer
transfer-encoding
upgrade
via
warning
Camel*
org.apache.camel*
With this filter, Camel wants to avoid that old HTTP headers are still present on outgoing requests (probably with wrong data).
The filtering of Camel headers is just to remove Camel specific stuff that is not relevant for HTTP.
Actually, the problem was the cxfrs component.
We manage to find an answer here : see : Response to REST client from Camel CXFRS route?
Here is the final solution.
Thanks to everyone that looked or answer, I hope that'll help someone else.
public class ModificationHeaderLinkProcessor implements Processor {
private static final String HEADER_LINK = "Link";
#Override
public void process(final Exchange exchange) {
List<String> newLinks= getNewLinks(exchange, oldUrl, newUrl);
ResponseBuilder builder = createHttpResponse(exchange);
builder.header(HEADER_LINK, newLinks);
exchange.getIn().setBody(builder.build());
}
private List<String> getNewLinks(final Exchange exchange, final String oldUrl, final String newUrl) {
ArrayList<String> oldLinks= exchange.getIn().getHeader(HEADER_LINK, ArrayList.class);
return oldLinks.stream().map(link-> link.replace(oldUrl, newUrl)).collect(toList());
}
private ResponseBuilder createHttpResponse(final Exchange exchange) {
ResponseBuilder builder = Response.status(getHttpStatusCode(exchange))
.entity(exchange.getIn().getBody());
clearUselessHeader(exchange);
exchange.getIn().getHeaders().forEach(builder::header);
return builder;
}
private void clearUselessHeader(final Exchange exchange) {
exchange.getIn().removeHeader(HEADER_LINK);
exchange.getIn().removeHeaders("Camel*");
}
private Integer getHttpStatusCode(final Exchange exchange) {
return exchange.getIn().getHeader(exchange.HTTP_RESPONSE_CODE, Integer.class);
}
private final String getPropertiesValue(CamelContext camelContext, String key) {
return camelContext.getPropertiesComponent().resolveProperty(key).orElseThrow();
}
}
Related
I work on small test project to check how Spring Reactive Web Applications actually works with MongoDB.
I follow the manual from https://docs.spring.io/spring/docs/5.0.0.M4/spring-framework-reference/html/web-reactive.html
and it states that I can process POST request in controller like:
#PostMapping("/person")
Mono<Void> create(#RequestBody Publisher<Person> personStream) {
return this.repository.save(personStream).then();
}
Though this seems not works. Here the controller I implemented:
https://github.com/pavelmorozov/reactor-poc/blob/master/src/main/java/com/springreactive/poc/controller/BanquetHallController.java
it have just one POST mapping and it is very simple:
#PostMapping("/BanquetHall")
Mono<Void> create(#RequestBody Publisher<BanquetHall> banquetHallStream) {
return banquetHallRepository.insert(banquetHallStream).then();
}
It is called each time I issue a POST with curl:
curl -v -XPOST -H "Content-type: application/json" -d '{"name":"BH22"}' 'http://localhost:8080/BanquetHall'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /BanquetHall HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> Accept: */*
> Content-type: application/json
> Content-Length: 15
>
* upload completely sent off: 15 out of 15 bytes
< HTTP/1.1 200 OK
< content-length: 0
<
* Connection #0 to host localhost left intact
And I see new objects stored in mongodb, but they not contain data. To debug I build simple subscriber, to see the data actually passed as request body to controller:
Subscriber s = new Subscriber() {
#Override
public void onSubscribe(Subscription s) {
logger.info("Argument: "+s.toString());
}
#Override
public void onNext(Object t) {
logger.info("Argument: "+t.toString());
}
#Override
public void onError(Throwable t) {
logger.info("Argument: "+t.toString());
}
#Override
public void onComplete() {
logger.info("Complete! ");
}
};
banquetHallStream.subscribe(s);
and now I see after subscription onError method called. The Throwable states body missing:
Here error string:
Request body is missing: reactor.core.publisher.Mono<java.lang.Void> com.springreactive.poc.controller.BanquetHallController.create(org.reactivestreams.Publisher<com.springreactive.poc.domain.BanquetHall>)
Why request body is empty?
Also good to know: As I new with all this reactive stuff, could it be some better approach to debug Publisher/Subscriber without manual implementing Subscriber?
Update I updated POST handler method description and it passes request body as String object:
Mono<Void> create(#RequestBody String banquetHallStream)
Then this is not a "Reactive", right? String is not reactive, as Publisher should be...
I had exact the same issue and was able to solve it by putting #ResponseStatus on method. Below is how method controller looks like:
#ResponseStatus(HttpStatus.CREATED)
#PostMapping(value = "/bulk", consumes = APPLICATION_STREAM_JSON_VALUE)
public Mono<Void> bulkInsert(#RequestBody Flux<Quote> quotes) {
return quoteReactiveRepository.insert(quotes).then();
}
I'm doing the request to that endpoint using WebClient:
webClient.post()
.uri("/quotes/bulk")
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(flux(), Quote.class)
.retrieve()
.bodyToMono(Void.class).block();
tested with: org.springframework.boot:spring-boot-starter-webflux:2.1.0.RELEASE
I have an authorization server [Simple Class annotated with #SpringBootApplication,
#RestController,#Configuration,#EnableAuthorizationServer & oauth2 security] running on port 8081 which works fine & provides the access token when requested from POSTMAN using POST method along with needful parameters in the form of key value pair,
http://localhost:8080/oauth/token, but how should i implement the camel route in java to get the access token by passing parameters in body ?
This question is more about sending multipart/form-data with Apache Camel. I was playing with it some time ago and solved it with custom Processor, converting headers to multipart/form-data format with Content-Disposition: form-data.
This is my Processor converting headers to multipart/form-data format:
public class PrepareMultipartFormData implements Processor {
private String[] multipartHeaders;
public PrepareMultipartFormData(String... multipartHeaders) {
this.multipartHeaders = multipartHeaders;
}
#Override
public void process(Exchange exchange) throws Exception {
addMultipart(exchange.getIn(), multipartHeaders);
}
private static void addMultipart(Message message, String... multipartKeys){
final String boundary = "---------------------------"+RandomStringUtils.randomAlphanumeric(9);
message.setHeader(Exchange.CONTENT_TYPE, "multipart/form-data;boundary="+boundary);
StringBuilder sb = new StringBuilder("--").append(boundary);
for (String key: multipartKeys) {
sb.append("\r\n")
.append("Content-Disposition: form-data; name=\"").append(key).append("\"")
.append("\r\n\r\n")
.append(message.getHeader(key, String.class))
.append("\r\n")
.append("--").append(boundary);
}
message.setBody(sb.toString());
}
}
To OAuth request token you need to send:
HTTP headers
Authorization header - This is part of standard HTTP component specified by endpoint options authUsername and authPassword
Content-Type - This is added in my PrepareMultipartFormData Processor
Form data - These are converted from headers in PrepareMultipartFormData Processor
grant_type
username
password
client_id
Final route can be implemented in this way:
(Replace constants with some expressions, to set it dynamically. If you need only token in response, add some unmarshalling, since this route returns JSON)
from("direct:getTokenResponse")
.setHeader(Exchange.HTTP_METHOD, constant("POST"))
.setHeader(Exchange.HTTP_PATH, constant("oauth/token"))
.setHeader("grant_type", constant("password"))
.setHeader("username", constant("admin"))
.setHeader("password", constant("admin1234"))
.setHeader("client_id", constant("spring-security-oauth2-read-write-client"))
.process(new PrepareMultipartFormData("grant_type", "username", "password", "client_id"))
.to("http://localhost:8080?authMethod=Basic&authUsername=oauth-endpoint-username&authPassword=oauth-endpoint-password")
.convertBodyTo(String.class)
.to("log:response");
Updating answer to provide a bit shorter implementation of PrepareMultipartFormData#addMultipart using MultipartEntityBuilder.
private static void addMultipart(Message message, String... multipartKeys) throws Exception{
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
for (String key: multipartKeys) {
builder.addTextBody(key, message.getHeader(key, String.class));
}
HttpEntity resultEntity = builder.build();
message.setHeader(Exchange.CONTENT_TYPE, resultEntity.getContentType().getValue());
message.setBody(resultEntity.getContent());
}
I've been debugging this for three hours, I still cannot explain why my custom headers (registered via a client request filter) are not sent.
The client is configured as such (full source here):
private WebTarget webTarget(String host, String appId, String appKey) {
return newClient(clientConfiguration(appId, appKey))
.target(host + "/rest");
}
private Configuration clientConfiguration(String appId, String appKey) {
ClientConfig config = new ClientConfig();
config.register(requestFilter(appId, appKey));
return config;
}
private ClientRequestFilter requestFilter(String appId, String appKey) {
return new VidalRequestFilter(apiCredentials(appId, appKey));
}
The filter is as follows:
public class VidalRequestFilter implements ClientRequestFilter {
private final ApiCredentials credentials;
public VidalRequestFilter(ApiCredentials credentials) {
this.credentials = credentials;
}
#Override
public void filter(ClientRequestContext requestContext) throws IOException {
MultivaluedMap<String, Object> headers = requestContext.getHeaders();
headers.add(ACCEPT, APPLICATION_ATOM_XML_TYPE);
headers.add("app_id", credentials.getApplicationId());
headers.add("app_key", credentials.getApplicationKey());
}
}
And the call is like:
String response = webTarget
.path("api/packages")
.request()
.get()
.readEntity(String.class);
All I get is 403 forbidden, because the specific endpoint I am calling is protected (the auth is performed with the custom headers defined above).
The weirdest thing is that, while I'm debugging, I see that sun.net.www.MessageHeader is properly invoked during the request write (i.e. the instance is valued as such: sun.net.www.MessageHeader#14f9390f7 pairs: {GET /rest/api/packages HTTP/1.1: null}{Accept: application/atom+xml}{app_id: XXX}{app_key: YYY}{User-Agent: Jersey/2.22.1 (HttpUrlConnection 1.8.0_45)}{Host: ZZZ}{Connection: keep-alive}.
However, I have the confirmation that neither our API server, nor its reverse proxy received GET requests with the required auth headers (a first HEAD request seems to be OK, though).
I know for sure the credentials are good 'cause the equivalent curl command just works!
I tried the straightforward approach to set headers directly when defining the call without any success.
What am I missing?
In Restlet 2.3 I am using a ChallengeAuthenticator with ChallengeScheme.HTTP_BASIC to protect application resources. When the server receives an incorrect set of credentials the server correctly returns a 401 Unauthorized response. Also correctly it adds the following header:
WWW-Authenticate → Basic realm="My security Realm"
The problem is when that response goes back to a browser rather than a server (as is the case with the AngularJS application GUI), the browser natively interprets that 401 response and launches an 'Authentication Required' modal.
What I would like to try and achieve is to read the request headers (easily done) and if the X-Requested-With: XMLHttpRequest header is present I would like to suppress the WWW-Authenticate header in the '401' response.
Currently the WWW-Authenticate header is automatically set so my question is how can I override this default header being set and handle it manually?
In your case, you should use a filter to remove the header WWW-Authenticate from the response. This header corresponds to a challenge request in the response.
Here is the content of the filter:
public class SecurityPostProcessingFilter extends Filter {
public SecurityPostProcessingFilter(
Context context, Restlet next) {
super(context, next);
}
#Override
protected void afterHandle(Request request, Response response) {
String requestedWith
= request.getHeaders().getFirstValue("X-Requested-With");
if ("XMLHttpRequest".equals(requestedWith)) {
response.getChallengeRequests().clear();
}
}
}
You need to add it within the createInboundRoot method of your Restlet application, as described below
public class RestletApplication extends Application {
(...)
#Override
public Restlet createInboundRoot() {
Router router = new Router(getContext());
(...)
ChallengeAuthenticator guard = new ChallengeAuthenticator(
null, ChallengeScheme.HTTP_BASIC, "testRealm");
(...)
guard.setNext(router);
Filter filter = new SecurityPostProcessingFilter(
getContext(), guard);
return filter;
}
}
This will remove the header WWW-Authenticate from the response when the value of the header X-Requested-From is equals to XMLHttpRequest in the request.
FYI, there is a page on the Restlet web site that describes the mapping between HTTP headers and the Restlet API: http://restlet.com/technical-resources/restlet-framework/guide/2.2/core/http-headers-mapping.
Hope it helps you,
Thierry
Another way is to override the ChallengeAuthenticator#challenge method.
By default it set the response status and add a challengeRequest:
ChallengeAuthenticator guard = new ChallengeAuthenticator(getContext(), ChallengeScheme.HTTP_BASIC, "realm") {
public void challenge(org.restlet.Response response, boolean stale) {
String requestedFrom = response.getRequest().getHeaders().getFirstValue("X-Requested-With");
if (!"XMLHttpRequest".equals(requestedFrom)) {
super.challenge(response, stale);
} else {
response.setStatus(Status.CLIENT_ERROR_UNAUTHORIZED);
}
};
};
I'm probably barking up the wrong tree with this, but I'm having some difficulty with Spring Integration and a http outbound-gateway.
I can configure it so that it makes a http POST and I get the response body as a simple String like this:
Spring Config
<int-http:outbound-gateway request-channel="hotelsServiceGateway.requestChannel"
reply-channel="hotelsServiceGateway.responseChannel"
url="http://api.ean.com/ean-services/rs/hotel/v3/list"
expected-response-type="java.lang.String"
http-method="POST"/>
Interface
public interface ExpediaHotelsService {
String getHotelsList(final Map<String, String> parameters);
}
And I can configure it so that I get a ResponseEntity back like this:
Spring Config
<int-http:outbound-gateway request-channel="hotelsServiceGateway.requestChannel"
reply-channel="hotelsServiceGateway.responseChannel"
url="http://api.ean.com/ean-services/rs/hotel/v3/list"
http-method="POST"/>
Interface
public interface ExpediaHotelsService {
ResponseEntity<String> getHotelsList(final Map<String, String> parameters);
}
Both versions of the code work. However, when returning a String I get the response body, but I don't get the http status and headers etc.
But when I use the ResponseEntity version I get the http status and headers, but I always get a null body via ResponseEntity#getBody
Is there anyway I can get both the body and the http status and headers?
(Ignoring the fact that the expedia hotels api returns JSON - at the moment I just want to get the raw body text)
Some further info which helps clarify the problem I am seeing. If I put a wire-tap on the response channel:
When I've configured it to return a simple String I get:
INFO: GenericMessage [payload={"HotelListResponse":{"EanWsError":{"itineraryId":-1,"handling":"RECOVERABLE","category":"AUTHENTICATION","exceptionConditionId":-1,"presentationMessage":"TravelNow.com cannot service this request.","verboseMessage":"Authentication failure. (cid=0; ipAddress=194.73.101.79)"},"customerSessionId":"2c9d7b43-3447-4b5e-ad87-54ce7a810041"}}, headers={replyChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#4d0f2471, errorChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#4d0f2471, Server=EAN, Connection=keep-alive, id=5e3cb978-9730-856e-1583-4a0847b8dc73, Content-Length=337, contentType=application/json, http_statusCode=200, Date=1433403827000, Content-Type=application/x-www-form-urlencoded, timestamp=1433403827133}]
You can see the full response body in the payload, and notice the Content-Length being set to 337
Conversely, when I use a ResponseEntity<String> I get:
INFO: GenericMessage [payload=<200 OK,{Transaction-Id=[5f3894df-0a8e-11e5-a43a-ee6fbd565000], Content-Type=[application/json], Server=[EAN], Date=[Thu, 04 Jun 2015 07:50:30 GMT], Content-Length=[337], Connection=[keep-alive]}>, headers={replyChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#4d0f2471, errorChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#4d0f2471, Server=EAN, Connection=keep-alive, id=9a598432-99c9-6a15-3451-bf9b1515491b, Content-Length=337, contentType=application/json, http_statusCode=200, Date=1433404230000, Content-Type=application/x-www-form-urlencoded, timestamp=1433404230465}]
The Content-Length is still set to 337, but there is no response body in the payload
Notice that you don't use any expected-response-type for the second case.
The RestTemplate works this way in case of no expected-response-type:
public ResponseEntityResponseExtractor(Type responseType) {
if (responseType != null && !Void.class.equals(responseType)) {
this.delegate = new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger);
}
else {
this.delegate = null;
}
}
#Override
public ResponseEntity<T> extractData(ClientHttpResponse response) throws IOException {
if (this.delegate != null) {
T body = this.delegate.extractData(response);
return new ResponseEntity<T>(body, response.getHeaders(), response.getStatusCode());
}
else {
return new ResponseEntity<T>(response.getHeaders(), response.getStatusCode());
}
}
As you see it really returns the ResponseEntity without body.
And Spring Integration can do nothing here on the matter...
From other side let's take a look if you really need a whole ResponseEntity as a reply back from the <int-http:outbound-gateway>.
Maybe headerMapper would be enough for you?.. For example http status is here already, even in your logs from the question:
Server=EAN, Connection=keep-alive, id=5e3cb978-9730-856e-1583-4a0847b8dc73, Content-Length=337, contentType=application/json, http_statusCode=200, Date=1433403827000, Content-Type=application/x-www-form-urlencoded,