Search code examples
lagom

Lagom http status code / header returned as json


I have a sample where I make a client request to debug token request to the FB api, and return the result to the client.
Depending on whether the access token is valid, an appropriate header should be returned:

@Override
public ServerServiceCall<LoginUser, Pair<ResponseHeader, String>> login() {
    return this::loginUser;
}

public CompletionStage<Pair<ResponseHeader, String>> loginUser(LoginUser user) {
    ObjectMapper jsonMapper = new ObjectMapper();
    String responseString = null;
    DebugTokenResponse.DebugTokenResponseData response = null;
    ResponseHeader responseHeader = null;

    try {
        response = fbClient.verifyFacebookToken(user.getFbAccessToken(), config.underlying().getString("facebook.app_token"));
        responseString = jsonMapper.writeValueAsString(response);

    } catch (ExecutionException | InterruptedException | JsonProcessingException e) {
        LOG.error(e.getMessage());

    }

    if (response != null) {
        if (!response.isValid()) {
            responseHeader = ResponseHeader.NO_CONTENT.withStatus(401);
        } else {
            responseHeader = ResponseHeader.OK.withStatus(200);
        }
    }

    return completedFuture(Pair.create(responseHeader, responseString));
}

However, the result I get is:

enter image description here

This isn't really what I expected. What I expect to receive is an error http status code of 401, and the json string as defined in the code.
Not sure why I would need header info in the response body.

There is also a strange error that occurs when I want to return a HeaderServiceCall:

enter image description here

I'm not sure if this is a bug, also I am a bit unclear about the difference between a ServerServiceCall and HeaderServiceCall.

Could someone help?


Solution

  • The types for HeaderServiceCall are defined this way:

    interface HeaderServiceCall<Request,Response>
    

    and

    CompletionStage<Pair<ResponseHeader,Response>> invokeWithHeaders(RequestHeader requestHeader,
                                                                     Request request)
    

    What this means is that when you define a response type, the return value should be a CompletionStage of a Pair of the ResponseHeader with the response type.

    In your code, the response type should be String, but you have defined it as Pair<ResponseHeader, String>, which means it expects the return value to be nested: CompletionStage<Pair<ResponseHeader,Pair<ResponseHeader, String>>>. Note the extra nested Pair<ResponseHeader, String>.

    When used with HeaderServiceCall, which requires you to implement invokeWithHeaders, you get a compilation error, which indicates the mismatched types. This is the error in your screenshot above.

    When you implement ServerServiceCall instead, your method is inferred to implement ServiceCall.invoke, which is defined as:

    CompletionStage<Response> invoke()
    

    In other words, the return type of the method does not expect the additional Pair<ResponseHeader, Response>, so your implementation compiles, but produces the incorrect result. The pair including the ResponseHeader is automatically serialized to JSON and returned to the client that way.

    Correcting the code requires changing the method signature:

    @Override
    public HeaderServiceCall<LoginUser, String> login() {
        return this::loginUser;
    }
    

    You also need to change the loginUser method to accept the RequestHeader parameter, even if it isn't used, so that it matches the signature of invokeWithHeaders:

    public CompletionStage<Pair<ResponseHeader, String>> loginUser(RequestHeader requestHeader, LoginUser user)
    

    This should solve your problem, but it would be more typical for a Lagom service to use domain types directly and rely on the built-in JSON serialization support, rather than serializing directly in your service implementation. You also need to watch out for null values. You shouldn't return a null ResponseHeader in any circumstances.

    @Override
    public ServerServiceCall<LoginUser, Pair<ResponseHeader, DebugTokenResponse.DebugTokenResponseData>> login() {
        return this::loginUser;
    }
    
    public CompletionStage<Pair<ResponseHeader, DebugTokenResponse.DebugTokenResponseData>> loginUser(RequestHeader requestHeader, LoginUser user) {
        try {
            DebugTokenResponse.DebugTokenResponseData response = fbClient.verifyFacebookToken(user.getFbAccessToken(), config.underlying().getString("facebook.app_token"));
            ResponseHeader responseHeader;
    
            if (!response.isValid()) {
                responseHeader = ResponseHeader.NO_CONTENT.withStatus(401);
            } else {
                responseHeader = ResponseHeader.OK.withStatus(200);
            }
            return completedFuture(Pair.create(responseHeader, response));
        } catch (ExecutionException | InterruptedException | JsonProcessingException e) {
            LOG.error(e.getMessage());
            throw e;    
        }
    }
    

    Finally, it appears that fbClient.verifyFacebookToken is a blocking method (it doesn't return until the call completes). Blocking should be avoided in a Lagom service call, as it has the potential to cause performance issues and instability. If this is code you control, it should be written to use a non-blocking style (that returns a CompletionStage). If not, you should use CompletableFuture.supplyAsync to wrap the call in a CompletionStage, and execute it in another thread pool.

    I found this example on GitHub that you might be able to adapt: https://github.com/dmbuchta/empty-play-authentication/blob/0a01fd1bd2d8ef777c6afe5ba313eccc9eb8b878/app/services/login/impl/FacebookLoginService.java#L59-L74