Spring cloud gateway with Spring cache and caffeine - spring

I have a spring cloud gateway which forwards the API rest requests to some microservices.
I would like to cache the response for specific requests.
For this reason I wrote this Filter
#Component
#Slf4j
public class CacheResponseGatewayFilterFactory extends AbstractGatewayFilterFactory<CacheResponseGatewayFilterFactory.Config> {
private final CacheManager cacheManager;
public CacheResponseGatewayFilterFactory(CacheManager cacheManager) {
super(CacheResponseGatewayFilterFactory.Config.class);
this.cacheManager = cacheManager;
}
#Override
public GatewayFilter apply(CacheResponseGatewayFilterFactory.Config config) {
final var cache = cacheManager.getCache("MyCache");
return (exchange, chain) -> {
final var path = exchange.getRequest().getPath();
if (nonNull(cache.get(path))) {
log.info("Return cached response for request: {}", path);
final var response = cache.get(path, ServerHttpResponse.class);
final var mutatedExchange = exchange.mutate().response(response).build();
return mutatedExchange.getResponse().setComplete();
}
return chain.filter(exchange).doOnSuccess(aVoid -> {
cache.put(path, exchange.getResponse());
});
};
}
When I call my rest endpoint, the first time I receive the right json, the second time I got an empty body.
What am I doing wrong?
EDIT
This is a screenshot of the exchange.getRequest() just before doing cache.put()

I solved it creating a GlobalFilter and a ServerHttpResponseDecorator. This code is caching all the responses regardless (it can be easily improved to cache only specific responses).
This is the code. However I think it can be improved. In case let me know.
#Slf4j
#Component
public class CacheFilter implements GlobalFilter, Ordered {
private final CacheManager cacheManager;
public CacheFilter(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
final var cache = cacheManager.getCache("MyCache");
final var cachedRequest = getCachedRequest(exchange.getRequest());
if (nonNull(cache.get(cachedRequest))) {
log.info("Return cached response for request: {}", cachedRequest);
final var cachedResponse = cache.get(cachedRequest, CachedResponse.class);
final var serverHttpResponse = exchange.getResponse();
serverHttpResponse.setStatusCode(cachedResponse.httpStatus);
serverHttpResponse.getHeaders().addAll(cachedResponse.headers);
final var buffer = exchange.getResponse().bufferFactory().wrap(cachedResponse.body);
return exchange.getResponse().writeWith(Flux.just(buffer));
}
final var mutatedHttpResponse = getServerHttpResponse(exchange, cache, cachedRequest);
return chain.filter(exchange.mutate().response(mutatedHttpResponse).build());
}
private ServerHttpResponse getServerHttpResponse(ServerWebExchange exchange, Cache cache, CachedRequest cachedRequest) {
final var originalResponse = exchange.getResponse();
final var dataBufferFactory = originalResponse.bufferFactory();
return new ServerHttpResponseDecorator(originalResponse) {
#NonNull
#Override
public Mono<Void> writeWith(#NonNull Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
final var flux = (Flux<? extends DataBuffer>) body;
return super.writeWith(flux.buffer().map(dataBuffers -> {
final var outputStream = new ByteArrayOutputStream();
dataBuffers.forEach(dataBuffer -> {
final var responseContent = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(responseContent);
try {
outputStream.write(responseContent);
} catch (IOException e) {
throw new RuntimeException("Error while reading response stream", e);
}
});
if (Objects.requireNonNull(getStatusCode()).is2xxSuccessful()) {
final var cachedResponse = new CachedResponse(getStatusCode(), getHeaders(), outputStream.toByteArray());
log.debug("Request {} Cached response {}", cacheKey.getPath(), new String(cachedResponse.getBody(), UTF_8));
cache.put(cacheKey, cachedResponse);
}
return dataBufferFactory.wrap(outputStream.toByteArray());
}));
}
return super.writeWith(body);
}
};
}
#Override
public int getOrder() {
return -2;
}
private CachedRequest getCachedRequest(ServerHttpRequest request) {
return CachedRequest.builder()
.method(request.getMethod())
.path(request.getPath())
.queryParams(request.getQueryParams())
.build();
}
#Value
#Builder
private static class CachedRequest {
RequestPath path;
HttpMethod method;
MultiValueMap<String, String> queryParams;
}
#Value
private static class CachedResponse {
HttpStatus httpStatus;
HttpHeaders headers;
byte[] body;
}
}

Related

Reading response body from ServerHttpResponse Spring cloud gateway

I am trying to read response body from ServerHttpResponse in a FilterFactory class that extents AbstractGatewayFilterFactory. The method executes, but I never see the log line printed. Is this the correct approach to read response ? If yes, what am I missing here ?
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest.Builder reqBuilder = exchange.getRequest().mutate();
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.map(dataBuffer -> {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
log.info("Response : {}", new String(content, StandardCharsets.UTF_8));
return bufferFactory.wrap(content);
}));
}
return super.writeWith(body);
}
};
long start = System.currentTimeMillis();
return chain.filter(exchange.mutate()
.request(reqBuilder.build())
.response(decoratedResponse)
.build());
};
}

How to decompress gzipped content in spring reactive?

While migrating my spring server from servlets to reactive I had to change all the filters in the code to WebFilter. One of the filters was decompressing gzipped content, but I couldn't do the same with the new WebFilter.
With servlets I wrapped the inputstream with a GzipInputStream. What is the best practice to do it with spring reactive?
Solution:
#Component
public class GzipFilter implements WebFilter {
private static final Logger LOG = LoggerFactory.getLogger(GzipFilter.class);
public static final String CONTENT_ENCODING = "content-encoding";
public static final String GZIP = "gzip";
public static final String UTF_8 = "UTF-8";
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (!isGzip(request)) {
return chain.filter(exchange);
}
else {
ServerHttpRequest mutatedRequest = new ServerHttpRequestWrapper(request);
ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
return chain.filter(mutatedExchange);
}
}
private boolean isGzip(ServerHttpRequest serverHttpRequest) {
String encoding = serverHttpRequest.getHeaders().getFirst(CONTENT_ENCODING);
return encoding != null && encoding.contains(GZIP);
}
private static class ServerHttpRequestWrapper implements ServerHttpRequest {
private ServerHttpRequest request;
public ServerHttpRequestWrapper(ServerHttpRequest request) {
this.request = request;
}
private static byte[] getDeflatedBytes(GZIPInputStream gzipInputStream) throws IOException {
StringWriter writer = new StringWriter();
IOUtils.copy(gzipInputStream, writer, UTF_8);
return writer.toString().getBytes();
}
#Override
public String getId() {
return request.getId();
}
#Override
public RequestPath getPath() {
return request.getPath();
}
#Override
public MultiValueMap<String, String> getQueryParams() {
return request.getQueryParams();
}
#Override
public MultiValueMap<String, HttpCookie> getCookies() {
return request.getCookies();
}
#Override
public String getMethodValue() {
return request.getMethodValue();
}
#Override
public URI getURI() {
return request.getURI();
}
#Override
public Flux<DataBuffer> getBody() {
Mono<DataBuffer> mono = request.getBody()
.map(dataBuffer -> dataBuffer.asInputStream(true))
.reduce(SequenceInputStream::new)
.map(inputStream -> {
try (GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream)) {
byte[] targetArray = getDeflatedBytes(gzipInputStream);
return new DefaultDataBufferFactory().wrap(targetArray);
}
catch (IOException e) {
throw new IllegalGzipRequest(String.format("failed to decompress gzip content. Path: %s", request.getPath()));
}
});
return mono.flux();
}
#Override
public HttpHeaders getHeaders() {
return request.getHeaders();
}
}
}
love #Yuval's solution!
My original idea was to convert Flux to a local file, and then decompress the local file.
But getting a file downloaded in Spring Reactive is too challenging. I googled a lot, and most of them are blocking way to get file, (e.g. Spring WebClient: How to stream large byte[] to file? and How to correctly read Flux<DataBuffer> and convert it to a single inputStream , none of them works...) which makes no sense and will throw error when calling block() in a reactive flow.
#Yuval saved my day! It works well for me!

Endpoint "/api-docs" doesn't work with custom GsonHttpMessageConverter

I migrated from Springfox Swagger to Springdoc OpenApi. I have added few lines in my configuration about springdoc:
springdoc:
pathsToMatch: /api/**
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
In configuration class MainConfig.kt I have following code:
val customGson: Gson = GsonBuilder()
.registerTypeAdapter(LocalDateTime::class.java, DateSerializer())
.registerTypeAdapter(ZonedDateTime::class.java, ZonedDateSerializer())
.addSerializationExclusionStrategy(AnnotationExclusionStrategy())
.enableComplexMapKeySerialization()
.setPrettyPrinting()
.create()
override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
converters.add(GsonHttpMessageConverter(customGson))
}
When I go to http://localhost:8013/swagger-ui.html (in configuration I have server.port: 8013) the page is not redirect to swagger-ui/index.html?url=/api-docs&validatorUrl=. But this is not my main problem :). When I go to swagger-ui/index.html?url=/api-docs&validatorUrl= I got page with this information:
Unable to render this definition
The provided definition does not specify a valid version field.
Please indicate a valid Swagger or OpenAPI version field. Supported version fields are swagger: "2.0" and those that match openapi: 3.0.n (for example, openapi: 3.0.0).
But when I go to http://localhost:8013/api-docs I have this result:
"{\"openapi\":\"3.0.1\",\"info\":{(...)}}"
I tried using default config and I commented configureMessageConverters() method and result of \api-docs now looks like normal JSON:
// 20191218134933
// http://localhost:8013/api-docs
{
"openapi": "3.0.1",
"info": {(...)}
}
I remember when I was using Springfox there was something wrong with serialization and my customGson had additional line: .registerTypeAdapter(Json::class.java, JsonSerializer<Json> { src, _, _ -> JsonParser.parseString(src.value()) })
I was wondering that I should have special JsonSerializer. After debugging my first thought was leading to OpenApi class in io.swagger.v3.oas.models package. I added this code: .registerTypeAdapter(OpenAPI::class.java, JsonSerializer<OpenAPI> { _, _, _ -> JsonParser.parseString("") }) to customGson and nothing changed... So, I was digging deeper...
After when I ran my Swagger tests:
#EnableAutoConfiguration
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#AutoConfigureMockMvc
#ExtendWith(SpringExtension::class)
#ActiveProfiles("test")
class SwaggerIntegrationTest(#Autowired private val mockMvc: MockMvc) {
#Test
fun `should display Swagger UI page`() {
val result = mockMvc.perform(MockMvcRequestBuilders.get("/swagger-ui/index.html"))
.andExpect(status().isOk)
.andReturn()
assertTrue(result.response.contentAsString.contains("Swagger UI"))
}
#Disabled("Redirect doesn't work. Check it later")
#Test
fun `should display Swagger UI page with redirect`() {
mockMvc.perform(MockMvcRequestBuilders.get("/swagger-ui.html"))
.andExpect(status().isOk)
.andExpect(MockMvcResultMatchers.content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
}
#Test
fun `should get api docs`() {
mockMvc.perform(MockMvcRequestBuilders.get("/api-docs"))
.andExpect(status().isOk)
.andExpect(MockMvcResultMatchers.content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.jsonPath("\$.openapi").exists())
}
}
I saw in console this:
MockHttpServletRequest:
HTTP Method = GET
Request URI = /api-docs
Parameters = {}
Headers = []
Body = null
Session Attrs = {}
Handler:
Type = org.springdoc.api.OpenApiResource
Method = org.springdoc.api.OpenApiResource#openapiJson(HttpServletRequest, String)
Next I check openapiJson in OpenApiResource and...
#Operation(hidden = true)
#GetMapping(value = API_DOCS_URL, produces = MediaType.APPLICATION_JSON_VALUE)
public String openapiJson(HttpServletRequest request, #Value(API_DOCS_URL) String apiDocsUrl)
throws JsonProcessingException {
calculateServerUrl(request, apiDocsUrl);
OpenAPI openAPI = this.getOpenApi();
return Json.mapper().writeValueAsString(openAPI);
}
OK, Jackson... I have disabled Jackson by #EnableAutoConfiguration(exclude = [(JacksonAutoConfiguration::class)]) because I (and my colleagues) prefer GSON, but it doesn't explain why serialization go wrong after adding custom GsonHttpMessageConverter. I have no idea what I made bad. This openapiJson() is endpoint and maybe it mess something... I don't know. I haven't any idea. Did you have a similar problem? Can you give some advice or hint?
PS. Sorry for my bad English :).
I had the same issue with a project written in Java, and I've just solved that by defining a filter to format my springdoc-openapi json documentation using Gson. I guess you can easily port this workaround to Kotlin.
#Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
ByteResponseWrapper byteResponseWrapper = new ByteResponseWrapper((HttpServletResponse) response);
ByteRequestWrapper byteRequestWrapper = new ByteRequestWrapper((HttpServletRequest) request);
chain.doFilter(byteRequestWrapper, byteResponseWrapper);
String jsonResponse = new String(byteResponseWrapper.getBytes(), response.getCharacterEncoding());
response.getOutputStream().write((new com.google.gson.JsonParser().parse(jsonResponse).getAsString())
.getBytes(response.getCharacterEncoding()));
}
#Override
public void destroy() {
}
static class ByteResponseWrapper extends HttpServletResponseWrapper {
private PrintWriter writer;
private ByteOutputStream output;
public byte[] getBytes() {
writer.flush();
return output.getBytes();
}
public ByteResponseWrapper(HttpServletResponse response) {
super(response);
output = new ByteOutputStream();
writer = new PrintWriter(output);
}
#Override
public PrintWriter getWriter() {
return writer;
}
#Override
public ServletOutputStream getOutputStream() {
return output;
}
}
static class ByteRequestWrapper extends HttpServletRequestWrapper {
byte[] requestBytes = null;
private ByteInputStream byteInputStream;
public ByteRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream inputStream = request.getInputStream();
byte[] buffer = new byte[4096];
int read = 0;
while ((read = inputStream.read(buffer)) != -1) {
baos.write(buffer, 0, read);
}
replaceRequestPayload(baos.toByteArray());
}
#Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
#Override
public ServletInputStream getInputStream() {
return byteInputStream;
}
public void replaceRequestPayload(byte[] newPayload) {
requestBytes = newPayload;
byteInputStream = new ByteInputStream(new ByteArrayInputStream(requestBytes));
}
}
static class ByteOutputStream extends ServletOutputStream {
private ByteArrayOutputStream bos = new ByteArrayOutputStream();
#Override
public void write(int b) {
bos.write(b);
}
public byte[] getBytes() {
return bos.toByteArray();
}
#Override
public boolean isReady() {
return false;
}
#Override
public void setWriteListener(WriteListener writeListener) {
}
}
static class ByteInputStream extends ServletInputStream {
private InputStream inputStream;
public ByteInputStream(final InputStream inputStream) {
this.inputStream = inputStream;
}
#Override
public int read() throws IOException {
return inputStream.read();
}
#Override
public boolean isFinished() {
return false;
}
#Override
public boolean isReady() {
return false;
}
#Override
public void setReadListener(ReadListener readListener) {
}
}
You will also have to register your filter only for your documentation url pattern.
#Bean
public FilterRegistrationBean<DocsFormatterFilter> loggingFilter() {
FilterRegistrationBean<DocsFormatterFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new DocsFormatterFilter());
registrationBean.addUrlPatterns("/v3/api-docs");
return registrationBean;
}

How to get string response from php using android volley JsonObjectRequest?

ctually when we call API and send request in JSON format we are expecting response also come into JSON format. But here back end team sending me response in String format therefore my onErrorResponse () method get called. Here my status code is 200. But due to format of response not executed onResponse () method. So will you please help me to handle this? Might be I have to use CustomRequest here. Any suggestoin will be appreciated. Thanks
public class SampleJsonObjTask {
public static ProgressDialog progress;
private static RequestQueue queue;
JSONObject main;
JsonObjectRequest req;
private MainActivity context;
private String prd,us,ver,fha,ve,ves,sz,cat,pa,h,t,en,pha,pur,dip;
public SampleJsonObjTask(MainActivity context, JSONObject main) {
progress = new ProgressDialog(context);
progress.setMessage("Loading...");
progress.setCanceledOnTouchOutside(false);
progress.setCancelable(false);
progress.show();
this.context = context;
this.main = main;
ResponseTask();
}
private void ResponseTask() {
if (queue == null) {
queue = Volley.newRequestQueue(context);
}
req = new JsonObjectRequest(Request.Method.POST, "", main,
new Response.Listener<JSONObject>() {
#Override
public void onResponse(JSONObject response) {
progress.dismiss();
Log.e("response","response--->"+response.toString());
}
}, new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error) {
progress.dismiss();//error.getMessage()
/*back end team sending me response in String format therefore my onErrorResponse () method get called. Here my status code is 200.*/
}
})
{
#Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> params = new HashMap<String, String>();
params.put("Content-Type", "application/json");
return params;
}
};
req.setRetryPolicy(new DefaultRetryPolicy(20 * 1000, 0, 1f));
queue.add(req);
}
}
Here the Response coming like string format that is Value OK,
com.android.volley.ParseError: org.json.JSONException: Value OK of type java.lang.String cannot be converted to JSONObject
You can use StringRequest for that:
StringRequest request = new StringRequest(StringRequest.Method.POST, url, new Response.Listener<String>() {
#Override
public void onResponse(String response) { }
}, new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error) {
}
}) {
#Override
public String getBodyContentType() {
return "application/json; charset=utf-8";
}
#Override
public byte[] getBody() {
try {
JSONObject jsonObject = new JSONObject();
/* fill your json here */
return jsonObject.toString().getBytes("utf-8");
} catch (Exception e) { }
return null;
}
};

RestTemplate and Cookie

I need to send an HTTP cookie, I'm using RestTemplate:
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("Cookie", "SERVERID=c52");
HttpEntity requestEntity = new HttpEntity(null, requestHeaders);
ResponseEntity responses = restTemplate.exchange(webService.getValidateUserUrl(),
HttpMethod.POST, requestEntity, String.class, mapValidateUser);
However, the receiving server doesn't see the cookie.
The default rest template does not use a persistent connetion, here is what I use.
public class StatefullRestTemplate extends RestTemplate
{
private final HttpClient httpClient;
private final CookieStore cookieStore;
private final HttpContext httpContext;
private final StatefullHttpComponentsClientHttpRequestFactory statefullHttpComponentsClientHttpRequestFactory;
public StatefullRestTemplate()
{
super();
HttpParams params = new BasicHttpParams();
HttpClientParams.setRedirecting(params, false);
httpClient = new DefaultHttpClient(params);
cookieStore = new BasicCookieStore();
httpContext = new BasicHttpContext();
httpContext.setAttribute(ClientContext.COOKIE_STORE, getCookieStore());
statefullHttpComponentsClientHttpRequestFactory = new StatefullHttpComponentsClientHttpRequestFactory(httpClient, httpContext);
super.setRequestFactory(statefullHttpComponentsClientHttpRequestFactory);
}
public HttpClient getHttpClient()
{
return httpClient;
}
public CookieStore getCookieStore()
{
return cookieStore;
}
public HttpContext getHttpContext()
{
return httpContext;
}
public StatefullHttpComponentsClientHttpRequestFactory getStatefulHttpClientRequestFactory()
{
return statefullHttpComponentsClientHttpRequestFactory;
}
}
public class StatefullHttpComponentsClientHttpRequestFactory extends HttpComponentsClientHttpRequestFactory
{
private final HttpContext httpContext;
public StatefullHttpComponentsClientHttpRequestFactory(HttpClient httpClient, HttpContext httpContext)
{
super(httpClient);
this.httpContext = httpContext;
}
#Override
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri)
{
return this.httpContext;
}
}
You can also extend the RestTemplate:
public class CookieRestTemplate extends RestTemplate {
#Override
protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
ClientHttpRequest request = super.createRequest(url, method);
request.getHeaders().add("Cookie", "SERVERID=c52");
return request;
}
}

Resources