Accessing response headers using a decorator in Armeria - armeria

I would like to add a decorator to my armeria client that checks each http response if a certain http header was returned:
builder.decorator((delegate, ctx, req) -> {
final HttpResponse response = delegate.execute(ctx, req);
final AggregatedHttpResponse r = response.aggregate().join();
for (Map.Entry<AsciiString, String> header : r.headers()) {
if ("warning".equalsIgnoreCase(header.getKey().toString())) {
throw new IllegalArgumentException("Detected usage of deprecated API for request "
+ req.toString() + ":\n" + header.getValue());
}
}
return response;
});
However, when starting my client it blocks on the join() call and waits forever. Is there a standard pattern for this in Armeria ? Presumably i cannot just block on the response in an interceptor, but i could not find a way to access the response headers otherwise. Using subscribe or toDuplicator did not work any better though.

There are two ways to achieve the desired behavior.
The first option is to aggregate the response asynchronously and then convert it back to an HttpResponse. The key APIs are AggregatedHttpResponse.toHttpResponse() and HttpResponse.from(CompletionStage):
builder.decorator(delegate, ctx, req) -> {
final HttpResponse res = delegate.serve(ctx, req);
return HttpResponse.from(res.aggregate().thenApply(r -> {
final ResponseHeaders headers = r.headers();
if (headers...) {
throw new IllegalArgumentException();
}
// Convert AggregatedHttpResponse back to HttpResponse.
return r.toHttpResponse();
}));
});
This approach is fairly simple and straightforward, but it doesn't work for a streaming response, because it waits until the complete response body is ready.
If your service returns a potentially large streaming response, you can use a FilteredHttpResponse to filter the response without aggregating anything:
builder.decorator(delegate, ctx, req) -> {
final HttpResponse res = delegate.serve(ctx, req);
return new FilteredHttpResponse(res) {
#Override
public HttpObject filter(HttpObject obj) {
// Ignore other objects like HttpData.
if (!(obj instanceof ResponseHeaders)) {
return obj;
}
final ResponseHeaders headers = (ResponseHeaders) obj;
if (headers...) {
throw new IllegalArgumentException();
}
return obj;
}
};
});
It's slightly more verbose than the first option, but it does not buffer the response in the memory, which is great for large streaming responses.
Ideally, in the future, we'd like to add more operators to HttpResponse or StreamMessage. Please stay tuned to this issue page and add any suggestions for better API: https://github.com/line/armeria/issues/3097

Related

Unit testing Armeria's decorator using context.log().whenComplete()

I have a subclass of SimpleDecoratingHttpService that contains something like this:
override fun serve(ctx: ServiceRequestContext, req: HttpRequest): HttpResponse {
ctx.log().whenComplete().thenAccept {
if (it.responseCause() == ...) {
// do stuff
}
}
return unwrap().serve(ctx, req)
}
I want to test the logic inside the whenComplete() callback. However, when writing tests like this:
myDecorator.serve(context, request).aggregate().join()
the log() future never completes. What do I need to do to ensure that the log() future eventually completes?
Emulating RequestLog completion
A RequestLog is completed by Armeria's networking layer, so just consuming an HttpRequest or HttpResponse will not complete a RequestLog. To complete it, you need to call the methods in RequestLogBuilder:
var myDecorator = new MySimpleDecoratingHttpService(...);
var ctx = ServiceRequestContext.of(
HttpRequest.of(HttpMethod.GET, "/hello"));
var req = ctx.request();
var res = myDecorator.serve(ctx, ctx.req).aggregate().join();
// Fill the log.
ctx.logBuilder().endRequest();
assert ctx.log().isRequestComplete();
ctx.logBuilder().responseHeaders(ResponseHeaders.of(200));
ctx.logBuilder().endResponse();
assert ctx.log().isComplete();
Armeria team uses the same technique for testing BraveService, so you might want to check it out as well at BraveServiceTest.java:161.
Testing with a real server
If your setup is too complex to use a mock, as an alternative approach, you can launch a real Armeria server so that Armeria fills the log for you. You can easily launch a server using ServerRule (JUnit 4) or ServerExtension (JUnit 5):
class MyJUnit5Test {
static final var serviceContexts =
new LinkedBlockingQueue<ServiceRequestContext>();
#RegisterExtension
static final var server = new ServerExtension() {
#Override
protected void configure(ServerBuilder sb) throws Exception {
sb.service("/hello", (ctx, req) -> HttpResponse.of(200));
sb.decorator(delegate -> new MySimpleDecoratingHttpService(delegate, ...));
// Record the ServiceRequestContext of each request.
sb.decorator((delegate, ctx, req) -> {
serviceContexts.add(ctx);
return delegate.serve(ctx, req);
});
}
};
#BeforeEach
void clearServiceContexts() {
serviceContexts.clear();
}
#Test
void test() {
// Send a real request.
var client = WebClient.of(server.httpUri());
var res = client.get("/hello").aggregate().join();
// Get the ServiceRequestContext and its log.
var ctx = serviceContexts.take();
var log = sctx.log().whenComplete().join();
// .. check `log` here ..
assertEquals(200, log.responseHeaders().status().code());
}
}

How to get a CompletableFuture from Jetty's HttpClient?

Is it possible to use issue an asynchronous HTTP request using Jetty and get back a CompletableFuture?
I read the docs but could not find any examples of doing so. I found internal usage of CompletableFuture but I couldn't figure out how to access it using the public API.
UPDATE: I need the CompletableFuture to return the response body as well (not just the response code and headers).
I have been using this with jetty client 9.4.x
var completable = new CompletableFuture<ContentResponse>();
client
.newRequest(uri)
.send(new CompletableFutureResponseListener(completable));
where
public class CompletableFutureResponseListener extends BufferingResponseListener {
private final CompletableFuture<ContentResponse> completable;
public CompletableFutureResponseListener(
CompletableFuture<ContentResponse> completable) {
this.completable = completable;
}
#Override
public void onComplete(Result result) {
if (result.isFailed()) {
completable.completeExceptionally(result.getFailure());
} else {
var response =
new HttpContentResponse(
result.getResponse(),
getContent(),
getMediaType(),
getEncoding());
completable.complete(response);
}
}
}
It's trivial to convert a CompleteListener into a CompletableFuture in this way:
CompletableFuture<Result> completable = new Promise.Completable<>();
httpClient.newRequest(...).send(result -> {
if (result.isFailed()) {
completable.completeExceptionally(result.getFailure());
} else {
completable.complete(result);
}
});
However, you are right that this may be done by HttpClient itself. Track this issue.

how to get the error response from MoyaError

similar to this but this time i need to retrieve the JSOn response of the server.
here is my existing code:
return Observable.create{ observer in
let _ = self.provider
.request(.getMerchantDetails(qrId: qrId))
.filterSuccessfulStatusCodes()
.mapJSON()
.subscribe(onNext: { response in
observer.onNext(RQRMerchant(json: JSON(response)))
}, onError: { error in
observer.onError(error)
})
return Disposables.create()
my question is: I can get the error response code 404 by error.localizedDescription But I also want to get the JSON response of the 404 HTTP request.
I've been faced with the same problem, and for me the easiest and cleanest solution was to extend MoyaError to include a property for the decoded error object. In my case I'm using Decodable objects, so you could write something like this for a decodable BackendError representing the error you may get from your server:
extension MoyaError {
public var backendError: BackendError? {
return response.flatMap {
try? $0.map(BackendError.self)
}
}
}
If you instead prefer to directly deal with JSON you can invoke the mapJSONmethod instead of mapping to a Decodable.
Then you just have to do the following to get the error information for non successful status codes:
onError: { error in
let backendError = (error as? MoyaError).backendError
}
Since the response of your server is also contained in a JSON, that means that your onNext emissions can be successful JSON responses or invalid JSON responses.
Check the validity of the response using do operator
You can check for the validity of the response by doing the following:
return Observable.create{ observer in
let _ = self.provider
.request(.getMerchantDetails(qrId: qrId))
.filterSuccessfulStatusCodes()
.mapJSON()
.do(onNext: { response in
let isValidResponse : Bool = false // check if response is valid
if !isValidResponse {
throw CustomError.reason
}
})
.subscribe(onNext: { response in
observer.onNext(RQRMerchant(json: JSON(response)))
}, onError: { error in
observer.onError(error)
})
return Disposables.create()
Use the do operator
Check if the onNext emission is indeed a valid emission
Throw an error if it is invalid, signifying that the observable operation has failed.
Response validation
To keep your response validation code in the right place, you can define a class function within your response class definition that verifies if it is valid or not:
class ResponseOfTypeA {
public class func isValid(response: ResponseOfTypeA) throws {
if errorConditionIsTrue {
throw CustomError.reason
}
}
}
So that you can do the following:
// Your observable sequence
.mapJSON()
.do(onNext: ResponseOfTypeA.isValid)
.subscribe(onNext: { response in
// the rest of your code
})

Retrofit 2.1 Internal Server Error with Post

I am using Retrofit 2.1 and when i am posting an object to my server, it gives me Internal server error with status code = 500, but i try to to post from my backend, it works like a charm, I am sure this is not server's problem.
Undoubtedly, i should use retrofit as a singleton:
//return api if not null
HereApi getApi(){
if (api == null) {
api = getRetrofit().create(HereApi.class);
}
return api;
}
//returns restadapter if not null
Retrofit getRetrofit(){
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl("my endpoint")
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
and this method that i post Here object:
void createHere(Here here){
List<Here> list = new ArrayList<>();
list.add(here);
Call<List<Here>> call = getApi().createHere(list);
call.enqueue(new Callback<List<Here>>() {
#Override
public void onResponse(Call<List<Here>> call, Response<List<Here>> response) {
Log.i(TAG, "onResponse: "+response.message());
}
#Override
public void onFailure(Call<List<Here>> call, Throwable t) {
}
});
}
I tried to post a list with single object inside and to post one object alone, but still status code is 500 ;*(
This is my api service interface:
public interface HereApi{
#GET("/lessons/")
Call<List<Lesson>> getLesson(#QueryMap Map<String,String> map);
#Headers({
"Content-Type: application/json",
"Vary: Accept"
})
#POST("/heres/")
Call<List<Here>> createHere(#Body List<Here> list);
#GET("/heres/")
Call<List<Here>> getHeres(#QueryMap Map<String,String> map);
}
I have written backend in Django + Django-rest-framework:
When I try to post from this, it just works:
I need your help guys, i have only one day to complete this project!!!
Hi I think there is a datetime conversation issue.
Use Jackson formating attonation in order to properly serialize datetime field.

Meteor how to use Meteor.wrapAsync with facebook Graph Api

I solved Graph Api asynchronous request by using fibers/future, which allows to give function result after predefined amount of time, downside of this solution is when facebook sends response faster than 1000ms it will wait anyway.
Is there any way to make Server Side function which returns graph api result right after response comes? I have found Meteor.wrapAsync could be helpful, but I'm not sure i get it's syntax correctly.
Here's what I have done using fibers and it's working exactly one second.
function graphGet(query){
var response = new Future(); // wait for async FB response
var waitingTime = 1000;
var graphResponse = "no result after: " + waitingTime + "ms";
FBGraph.get(query, function(error, response) {
if (response) { graphResponse = response; }
else { graphResponse = error; }
});
setTimeout(function() {
response['return'](graphResponse);
}, waitingTime);
return response.wait();
}
The same code using Meteor.wrapAsync is much shorter :
function graphGet(query){
// wrap the async func into a FBGraph bound sync version
var fbGraphGetSync = Meteor.wrapAsync(FBGraph.get, FBGraph);
// use a try / catch block to differentiate between error and success
try{
var result = fbGraphGetSync(query);
return result;
}
catch(exception){
console.log(exception);
}
}