Cache resources based on request payload
MODIFY
gateway/engine/policies/src/main/java/io/apiman/gateway/engine/policies/CachingResourcesPolicy.java
gateway/engine/policies/src/main/java/io/apiman/gateway/engine/policies/CachingResourcesPolicy.java
@@ -9,6 +9,7 @@ import io.apiman.gateway.engine.async.IAsyncResultHandler;
import io.apiman.gateway.engine.beans.ApiRequest;
import io.apiman.gateway.engine.beans.ApiResponse;
import io.apiman.gateway.engine.beans.exceptions.ComponentNotFoundException;
+import io.apiman.gateway.engine.beans.util.HeaderMap;
import io.apiman.gateway.engine.components.ICacheStoreComponent;
import io.apiman.gateway.engine.impl.CachedResponse;
import io.apiman.gateway.engine.io.AbstractStream;
@@ -19,10 +20,9 @@ import io.apiman.gateway.engine.io.ISignalWriteStream;
import io.apiman.gateway.engine.policies.caching.CacheConnectorInterceptor;
import io.apiman.gateway.engine.policies.config.CachingResourcesConfig;
import io.apiman.gateway.engine.policies.config.CachingResourcesSettingsEntry;
-import io.apiman.gateway.engine.policy.IConnectorInterceptor;
-import io.apiman.gateway.engine.policy.IDataPolicy;
-import io.apiman.gateway.engine.policy.IPolicyChain;
-import io.apiman.gateway.engine.policy.IPolicyContext;
+import io.apiman.gateway.engine.io.IPayloadIO;
+import io.apiman.gateway.engine.policy.*;
+import org.apache.commons.codec.digest.DigestUtils;
/**
* Policy that enables caching for back-end APIs responses.
@@ -77,7 +77,7 @@ public class CachingResourcesPolicy extends AbstractMappedDataPolicy<CachingReso
if (possibleMatchingEntries.size() > 0) {
// Check to see if there is a cache entry for this request.
// If so, we deliver the cached result by CacheConnectorInterceptor
- String cacheId = buildCacheID(request);
+ String cacheId = buildCacheID(request, context);
context.setAttribute(CACHE_ID_ATTR, cacheId);
ICacheStoreComponent cache = context.getComponent(ICacheStoreComponent.class);
cache.getBinary(cacheId, ApiResponse.class,
@@ -89,6 +89,7 @@ public class CachingResourcesPolicy extends AbstractMappedDataPolicy<CachingReso
} else {
ISignalReadStream<ApiResponse> cacheEntry = result.getResult();
if (cacheEntry != null) {
+ markCacheEntryAsCached(cacheEntry, config);
context.setConnectorInterceptor(new CacheConnectorInterceptor(cacheEntry));
context.setAttribute(SHOULD_CACHE_ATTR, Boolean.FALSE);
context.setAttribute(CACHED_RESPONSE, cacheEntry.getHead());
@@ -105,6 +106,18 @@ public class CachingResourcesPolicy extends AbstractMappedDataPolicy<CachingReso
}
}
+ /**
+ * Set or overwrite Cache-Control header with ttl as max-age to mark the response as cached
+ * @param cacheEntry
+ * @param config
+ */
+ private void markCacheEntryAsCached(ISignalReadStream<ApiResponse> cacheEntry, final CachingResourcesConfig config) {
+ if(cacheEntry.getHead() != null && cacheEntry.getHead() instanceof ApiResponse && cacheEntry.getHead().getHeaders() != null) {
+ HeaderMap responseHeaders = cacheEntry.getHead().getHeaders();
+ responseHeaders.put("Cache-Control", "max-age=" + String.valueOf(config.getTtl()));
+ }
+ }
+
/**
* Verify policy entry against request value and ensure that only cachable http methods are cached.
* @param policyEntry
@@ -199,7 +212,7 @@ public class CachingResourcesPolicy extends AbstractMappedDataPolicy<CachingReso
* verb and the destination. In the case where there's no API key the ID
* will contain ApiOrgId + ApiId + ApiVersion
*/
- private static String buildCacheID(ApiRequest request) {
+ private static String buildCacheID(ApiRequest request, IPolicyContext context) {
StringBuilder cacheId = new StringBuilder();
if (request.getContract() != null) {
cacheId.append(request.getApiKey());
@@ -216,6 +229,18 @@ public class CachingResourcesPolicy extends AbstractMappedDataPolicy<CachingReso
cacheId.append("?").append(request.getQueryParams().toQueryString());
}
+ //use hashed payload to ensure right caching of request with different bodies.
+ IPayloadIO requestPayloadIO = context.getAttribute(PolicyContextKeys.REQUEST_PAYLOAD_IO, null);
+ Object requestPayload = context.getAttribute(PolicyContextKeys.REQUEST_PAYLOAD, null);
+ if (requestPayloadIO != null && requestPayload != null) {
+ try {
+ byte[] payloadBytes = requestPayloadIO.marshall(requestPayload);
+ cacheId.append('_').append(DigestUtils.sha256Hex(payloadBytes));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
return cacheId.toString();
}

MODIFY
gateway/engine/policies/src/test/java/io/apiman/gateway/engine/policies/CachingResourcesPolicyTest.java
gateway/engine/policies/src/test/java/io/apiman/gateway/engine/policies/CachingResourcesPolicyTest.java
@@ -17,6 +17,7 @@ package io.apiman.gateway.engine.policies;
import io.apiman.test.common.mock.EchoResponse;
import io.apiman.test.policies.*;
+import io.apiman.gateway.engine.beans.Api;
import org.junit.Test;
import static org.junit.Assert.*;
@@ -213,6 +214,21 @@ public class CachingResourcesPolicyTest extends ApimanPolicyTest {
assertRequestIsCached(requestCache2);
}
+ @Test
+ @Configuration("{" +
+ " \"ttl\" : 5," +
+ " \"cachingResourcesSettingsEntries\" : [{\"statusCode\": \"*\", \"pathPattern\": \".*\", \"httpMethod\": \"*\"}]" +
+ "}")
+ public void testPostRequestCaching() throws Throwable {
+ PolicyTestRequest requestCache = PolicyTestRequest.build(PolicyTestRequestType.POST, "/testPostCache");
+ requestCache.body("Test");
+ Long counterValue = doRequest(requestCache);
+
+ requestCache.body("NotCached");
+ Long shouldNotCachedcounterValue = doRequest(requestCache);
+ assertNotEquals(counterValue, shouldNotCachedcounterValue);
+ }
+
/**
* Ensures that a request is cached.
* @param request

MODIFY
manager/ui/war/plugins/api-manager/html/policyForms/caching-resources.include
manager/ui/war/plugins/api-manager/html/policyForms/caching-resources.include
+<style>
+ .alert-warning-post {
+ margin-top: 60px;
+ }
+</style>
<div class="form policy-config caching" data-field="form" ng-controller="Apiman.CachingResourcesFormController" style="margin-top: 10px">
<div class="panel panel-default">
<div class="panel-heading">
</div>
</div>
</div>
-
<div>
<dl>
<dt apiman-i18n-key="configured-cached-resources-rules">Cached Resources</dt>
</dl>
</div>
-
+ <div class="alert alert-warning alert-warning-post" ng-show="isPostRequestCached()">
+ <span class="pficon pficon-warning-triangle-o"></span>
+ <span apiman-i18n-key="cached-resources-rules-payload-inspection-warning">Ensure that stateful request payload inspection is enabled if you want to cache POST requests.</span>
+ <span ng-show="pluginName && version.api.organization.id && version.api.id && version.version">
+ (<span apiman-i18n-key="cached-resources-rules-payload-inspection-warning-link-prefix">See</span> <a href="{{ pluginName }}/orgs/{{ version.api.organization.id }}/apis/{{ version.api.id }}/{{ version.version }}/impl" target="_blank" apiman-i18n-key="cached-resources-rules-payload-inspection-warning-link-text">API Implementation Tab</a>
+ )</span>
+ <span ng-show="!pluginName || !version.api.organization.id || !version.api.id || !version.version">
+ (<span apiman-i18n-key="cached-resources-rules-payload-inspection-warning-link-prefix">See</span> <span apiman-i18n-key="cached-resources-rules-payload-inspection-warning-link-text">API Implementation Tab</span>)</span>
+ </div>
<table class="table table-striped table-bordered">
<thead>
<tr>
</tr>
</tbody>
</table>
-
</div>

MODIFY
manager/ui/war/plugins/api-manager/ts/forms/new-policy.ts
manager/ui/war/plugins/api-manager/ts/forms/new-policy.ts
@@ -21,7 +21,12 @@ module Apiman {
($q, $location, $scope, OrgSvcs, ApimanSvcs, PageLifecycle, Logger, $routeParams) => {
var params = $routeParams;
+ var etype = params.type;
+
var pageData = {
+ version: $q(function(resolve, reject) {
+ OrgSvcs.get({ organizationId: params.org, entityType: etype, entityId: params.id, versionsOrActivity: 'versions', version: params.ver }, resolve, reject);
+ }),
policyDefs: $q(function(resolve, reject) {
ApimanSvcs.query({ entityType: 'policyDefs' }, function(policyDefs) {
$scope.selectedDefId = null;

MODIFY
manager/ui/war/plugins/api-manager/ts/policies.ts
manager/ui/war/plugins/api-manager/ts/policies.ts
@@ -690,7 +690,7 @@ module Apiman {
// Check that TTL & Policy Definition ID are set
valid = ((config.ttl && config.ttl > 0) && ($scope.selectedDef && $scope.selectedDef.id != null));
- valid = valid && config.cachingResourcesSettingsEntries.length > 0;
+ valid = valid && config.cachingResourcesSettingsEntries && config.cachingResourcesSettingsEntries.length > 0;
}
$scope.setValid(valid);
@@ -722,6 +722,15 @@ module Apiman {
};
$scope.isEntityDisabled = EntityStatusSvc.isEntityDisabled;
+
+ $scope.isPostRequestCached = function () {
+ if ($scope.config.cachingResourcesSettingsEntries != null) {
+ return $scope.config.cachingResourcesSettingsEntries.some((e) => {
+ return e.httpMethod === "POST" || e.httpMethod === "*";
+ });
+ }
+ return false;
+ }
}]);
_module.controller('Apiman.URLRewritingFormController',

MODIFY
manager/ui/war/src/main/resources/io/apiman/manager/ui/server/i18n/template_messages.properties
manager/ui/war/src/main/resources/io/apiman/manager/ui/server/i18n/template_messages.properties
@@ -209,6 +209,9 @@ break-contract=Break Contract
build-date-label=Build Date:
cached-http-method=HTTP Method
cached-path-pattern=Path Pattern
+cached-resources-rules-payload-inspection-warning=Ensure that stateful request payload inspection is enabled if you want to cache POST requests. (See
+cached-resources-rules-payload-inspection-warning-link-text=API Implementation Tab
+cached-resources-rules-payload-inspection-warning-link-prefix=See
cached-status-code=Status Code
caching.config-sentence-preamble=Cache API responses for
caching.enter-num-seconds.placeholder=Enter time-to-live

MODIFY
test/policies/src/main/java/io/apiman/test/policies/PolicyTester.java
test/policies/src/main/java/io/apiman/test/policies/PolicyTester.java
@@ -162,6 +162,7 @@ public class PolicyTester extends BlockJUnit4ClassRunner {
api.setVersion(String.valueOf(version));
api.setPublicAPI(true);
api.setApiPolicies(Collections.singletonList(policy));
+ api.setParsePayload(true);
getEngine().getRegistry().publishApi(api, new IAsyncResultHandler<Void>() {
@Override

Cache resources based on request payload (#749)