Stockpile — Store Request and Response

One Piece — Whaaaat? You don’t use Stock Pile?

For most applications, it is quite crucial to store the request and response of APIs, especially while integrating with external service providers. Logging this information solves for most of the use cases, but might not when the request/ response contains sensitive information.

In such cases, one way would be to encrypt and store both the request and the response, this is exactly what we will be doing in this post.

Overview :

  • Methods/ commands whose request and response are to be stored are annotated with @StockPile.
  • Arguments of the method to be stored are annotated with @Item.
  • Method interceptor to encrypt and store the request, response/ exception of these methods.
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD})
@BindingAnnotation
public @interface StockPile {
}

@StockPile annotation is only for methods, @Target – METHOD

@Retention(RetentionPolicy.RUNTIME)
@Target({FIELD, PARAMETER})
@BindingAnnotation
public @interface Item {
}

@Item is for arguments of a method, @Target – PARAMETER

@Slf4j
public class StockPileModule extends AbstractModule {

    @Override
    protected void configure() {
        final StockPileInterceptor stockPileInterceptor = new StockPileInterceptor();
        requestInjection(stockPileInterceptor);
        bindInterceptor(Matchers.any(), Matchers.annotatedWith(StockPile.class), stockPileInterceptor);
    }

    @VisibleForTesting
    public static class StockPileInterceptor implements MethodInterceptor {

        @VisibleForTesting
        @Inject
        protected YourEncryptionService encryptionService;

        @VisibleForTesting
        @Inject
        protected YourStorageService storageDao;

        @VisibleForTesting
        @Inject
        protected ObjectMapper objectMapper;

        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            final Optional<Annotation> optionalAnnotation = Stream.of(invocation.getMethod().getDeclaredAnnotations())
                    .filter(annotation -> annotation instanceof StockPile)
                    .findFirst();

            if (optionalAnnotation.isPresent()) {
                List<Object> requests = new ArrayList<>();
                final String methodName = invocation.getMethod().getName();
                final Annotation[][] parameterAnnotations = invocation.getMethod().getParameterAnnotations();

                for (int index = 0; index < parameterAnnotations.length; ++index) {
                    for (final Annotation annotation : parameterAnnotations[index]) {
                        if (annotation instanceof Item) {
                            requests.add(invocation.getArguments()[index]);
                        }
                    }
                }
                final Object response;
                try {
                    response = invocation.proceed();
                    storageDao.save(requests, response, methodName);
                } catch (Exception e) {
                    storageDao.save(requests, e, methodName);
                    throw e;
                }
                return response;
            }
            return invocation.proceed();
        }
    }   
}

The scope of this post, is to generalise the storage of request and response. It’s upto to the developer to decide what sort of encryption and data store for storage of these blobs (NoSQL such as AeroSpike is a good option to consider) would be.

As seen in the method interceptor above, we first validate if the method is annotated with @StockPile, then all the arguments annotated with @Item is added to a List<Object>

storageDao.save takes three arguments:

  • Request (Object)
  • Response or Exception (Object)
  • Method name (String)
private void save(Object request, Object response, String commandName) {
    try {
        final String userId = MDC.get(RequestContext.REQUEST_USER_ID);
        final String requestId = MDC.get(RequestContext.REQUEST_ID);
        
        final String workflowId = Objects.nonNull(REQUEST_USER_ID) ? userId.concat("_").concat(requestId) : userId;
        final Date currentDate = new Date();

        Optional<RequestResponseBlob> requestResponseBlob = storageDao
                .get(workflowId, commandName);

        final byte[] encryptedRequest = objectMapper.writeValueAsBytes(encryptionService.encrypt(request));
        final byte[] encryptedResponse = objectMapper.writeValueAsBytes(encryptionService.encrypt(response));

        if (storedOutboundMessage.isPresent()) {
            RequestResponseBlob presentRequestResponse = requestResponseBlob.get();
            presentRequestResponse.setRequest(encryptedRequest);
            presentRequestResponse.setResponse(encryptedResponse);
            presentRequestResponse.setUpdated(currentDate);
            storageDao.update(presentRequestResponse);
        } else {
            storageDao.save(RequestResponseBlob.builder()
                    .commandType(commandName)
                    .workflowId(workflowId)
                    .requestId(requestId)
                    .request(encryptedRequest)
                    .response(encryptedResponse)
                    .created(currentDate)
                    .updated(currentDate)
                    .build());
        }
    } catch (Exception e) {
        log.error("[STOCKPILE] Error while storing");
    }
}

Refer to the previous post for storing userId and requestId in MDC. For the sake of querying, using userId, requestId along with the method name would be ideal as shown above.

Usage:

@StockPile
public Response externalServiceCommandOne(@Item final SensitiveInformationRequest request, final String token) {
  Response response;
  // Stuff here
  return response
}

That’s it! Done 🚀

Cite this article as: Adesh Nalpet Adimurthy. (Jan 19, 2022). Stockpile — Store Request and Response. PyBlog. https://www.pyblog.xyz/stock-pile

#index table of contents