From a10b5d0491ead66a150d705391418282168c6193 Mon Sep 17 00:00:00 2001 From: Graham Leggett Date: Tue, 28 May 2013 20:30:04 +0000 Subject: [PATCH] mod_cache: Invalidate cached entities in response to RFC2616 Section 13.10 Invalidation After Updates or Deletions. PR 15868 trunk patch: http://svn.apache.org/r1070179 http://svn.apache.org/r1478140 http://svn.apache.org/r1478173 http://svn.apache.org/r1478798 2.4.x patch: http://people.apache.org/~minfrin/httpd-mod_cache-invalidate7.patch Submitted by: minfrin Reviewed by: jim, wrowe git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x@1487102 13f79535-47bb-0310-9956-ffa450edef68 --- CHANGES | 4 + STATUS | 9 -- include/ap_mmn.h | 3 +- modules/cache/cache_common.h | 1 + modules/cache/cache_storage.c | 169 ++++++++++++++++++++++++----- modules/cache/cache_storage.h | 14 +++ modules/cache/cache_util.c | 6 +- modules/cache/mod_cache.c | 172 ++++++++++++++++++++++++++---- modules/cache/mod_cache_disk.c | 12 ++- modules/cache/mod_cache_socache.c | 5 +- 10 files changed, 331 insertions(+), 64 deletions(-) diff --git a/CHANGES b/CHANGES index a9ed1ec32d..8fd76ed71e 100644 --- a/CHANGES +++ b/CHANGES @@ -2,6 +2,10 @@ Changes with Apache 2.4.5 + *) mod_cache: Invalidate cached entities in response to RFC2616 Section + 13.10 Invalidation After Updates or Deletions. PR 15868 [Graham + Leggett] + *) mod_dav: Improve error handling in dav_method_put(), add new dav_join_error() function. PR 54145. [Ben Reser ] diff --git a/STATUS b/STATUS index 88c67b6198..8048fdffe5 100644 --- a/STATUS +++ b/STATUS @@ -90,15 +90,6 @@ RELEASE SHOWSTOPPERS: PATCHES ACCEPTED TO BACKPORT FROM TRUNK: [ start all new proposals below, under PATCHES PROPOSED. ] - * mod_cache: Invalidate cached entities in response to RFC2616 Section - 13.10 Invalidation After Updates or Deletions. PR 15868 - trunk patch: http://svn.apache.org/r1070179 - http://svn.apache.org/r1478140 - http://svn.apache.org/r1478173 - http://svn.apache.org/r1478798 - 2.4.x patch: http://people.apache.org/~minfrin/httpd-mod_cache-invalidate7.patch - +1: minfrin, jim, wrowe - * mod_cache: Ignore response headers specified by no-cache=header and private=header as specified by RFC2616 14.9.1 What is Cacheable. Ensure that these headers are still processed when multiple Cache-Control diff --git a/include/ap_mmn.h b/include/ap_mmn.h index 7087fba9a2..6d3ce53469 100644 --- a/include/ap_mmn.h +++ b/include/ap_mmn.h @@ -405,6 +405,7 @@ * 20120211.13 (2.4.5-dev) Add ap_get_exec_line * 20120211.14 (2.4.5-dev) Add ppinherit and inherit to proxy_server_conf * 20120211.15 (2.4.5-dev) Add dav_join_error() + * 20120211.16 (2.4.5-dev) Add cache_control_t.invalidated */ #define MODULE_MAGIC_COOKIE 0x41503234UL /* "AP24" */ @@ -412,7 +413,7 @@ #ifndef MODULE_MAGIC_NUMBER_MAJOR #define MODULE_MAGIC_NUMBER_MAJOR 20120211 #endif -#define MODULE_MAGIC_NUMBER_MINOR 15 /* 0...n */ +#define MODULE_MAGIC_NUMBER_MINOR 16 /* 0...n */ /** * Determine if the server's current MODULE_MAGIC_NUMBER is at least a diff --git a/modules/cache/cache_common.h b/modules/cache/cache_common.h index cedce0767d..9d56d28b66 100644 --- a/modules/cache/cache_common.h +++ b/modules/cache/cache_common.h @@ -45,6 +45,7 @@ typedef struct cache_control { unsigned int must_revalidate:1; unsigned int proxy_revalidate:1; unsigned int s_maxage:1; + unsigned int invalidated:1; /* has this entity been invalidated? */ apr_int64_t max_age_value; /* if positive, then set */ apr_int64_t max_stale_value; /* if positive, then set */ apr_int64_t min_fresh_value; /* if positive, then set */ diff --git a/modules/cache/cache_storage.c b/modules/cache/cache_storage.c index dc51ca5b4d..1267c5da0e 100644 --- a/modules/cache/cache_storage.c +++ b/modules/cache/cache_storage.c @@ -390,14 +390,15 @@ int cache_select(cache_request_rec *cache, request_rec *r) return DECLINED; } -apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, - const char **key) +static apr_status_t cache_canonicalise_key(request_rec *r, apr_pool_t* p, + const char *uri, apr_uri_t *parsed_uri, const char **key) { cache_server_conf *conf; char *port_str, *hn, *lcs; const char *hostname, *scheme; int i; - char *path, *querystring; + const char *path; + char *querystring; if (*key) { /* @@ -411,7 +412,7 @@ apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, * option below. */ conf = (cache_server_conf *) ap_get_module_config(r->server->module_config, - &cache_module); + &cache_module); /* * Use the canonical name to improve cache hit rate, but only if this is @@ -437,15 +438,15 @@ apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, } else { /* Use _default_ as the hostname if none present, as in mod_vhost */ - hostname = ap_get_server_name(r); + hostname = ap_get_server_name(r); if (!hostname) { hostname = "_default_"; } } } - else if(r->parsed_uri.hostname) { + else if (parsed_uri->hostname) { /* Copy the parsed uri hostname */ - hn = apr_pstrdup(p, r->parsed_uri.hostname); + hn = apr_pstrdup(p, parsed_uri->hostname); ap_str_tolower(hn); /* const work-around */ hostname = hn; @@ -464,9 +465,9 @@ apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, * "no proxy request" and "reverse proxy request" are handled in the same * manner (see above why this is needed). */ - if (r->proxyreq && r->parsed_uri.scheme) { + if (r->proxyreq && parsed_uri->scheme) { /* Copy the scheme and lower-case it */ - lcs = apr_pstrdup(p, r->parsed_uri.scheme); + lcs = apr_pstrdup(p, parsed_uri->scheme); ap_str_tolower(lcs); /* const work-around */ scheme = lcs; @@ -489,11 +490,11 @@ apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, * server. */ if (r->proxyreq && (r->proxyreq != PROXYREQ_REVERSE)) { - if (r->parsed_uri.port_str) { - port_str = apr_pcalloc(p, strlen(r->parsed_uri.port_str) + 2); + if (parsed_uri->port_str) { + port_str = apr_pcalloc(p, strlen(parsed_uri->port_str) + 2); port_str[0] = ':'; - for (i = 0; r->parsed_uri.port_str[i]; i++) { - port_str[i + 1] = apr_tolower(r->parsed_uri.port_str[i]); + for (i = 0; parsed_uri->port_str[i]; i++) { + port_str[i + 1] = apr_tolower(parsed_uri->port_str[i]); } } else if (apr_uri_port_of_scheme(scheme)) { @@ -525,13 +526,13 @@ apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, * Check if we need to ignore session identifiers in the URL and do so * if needed. */ - path = r->uri; - querystring = r->parsed_uri.query; + path = uri; + querystring = parsed_uri->query; if (conf->ignore_session_id->nelts) { int i; char **identifier; - identifier = (char **)conf->ignore_session_id->elts; + identifier = (char **) conf->ignore_session_id->elts; for (i = 0; i < conf->ignore_session_id->nelts; i++, identifier++) { int len; char *param; @@ -542,9 +543,9 @@ apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, * of the path and that the parameter matches our identifier */ if ((param = strrchr(path, ';')) - && !strncmp(param + 1, *identifier, len) - && (*(param + len + 1) == '=') - && !strchr(param + len + 2, '/')) { + && !strncmp(param + 1, *identifier, len) + && (*(param + len + 1) == '=') + && !strchr(param + len + 2, '/')) { path = apr_pstrndup(p, path, param - path); continue; } @@ -557,7 +558,7 @@ apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, * querystring and followed by a '=' */ if (!strncmp(querystring, *identifier, len) - && (*(querystring + len) == '=')) { + && (*(querystring + len) == '=')) { param = querystring; } else { @@ -579,14 +580,15 @@ apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, if (querystring != param) { querystring = apr_pstrndup(p, querystring, - param - querystring); + param - querystring); } else { querystring = ""; } if ((amp = strchr(param + len + 1, '&'))) { - querystring = apr_pstrcat(p, querystring, amp + 1, NULL); + querystring = apr_pstrcat(p, querystring, amp + 1, + NULL); } else { /* @@ -606,12 +608,12 @@ apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, /* Key format is a URI, optionally without the query-string */ if (conf->ignorequerystring) { - *key = apr_pstrcat(p, scheme, "://", hostname, port_str, - path, "?", NULL); + *key = apr_pstrcat(p, scheme, "://", hostname, port_str, path, "?", + NULL); } else { - *key = apr_pstrcat(p, scheme, "://", hostname, port_str, - path, "?", querystring, NULL); + *key = apr_pstrcat(p, scheme, "://", hostname, port_str, path, "?", + querystring, NULL); } /* @@ -622,9 +624,118 @@ apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, * resource in the cache under a key where it is never found by the quick * handler during following requests. */ - ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, APLOGNO(00698) - "cache: Key for entity %s?%s is %s", r->uri, - r->parsed_uri.query, *key); + ap_log_rerror( + APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, APLOGNO(00698) "cache: Key for entity %s?%s is %s", uri, parsed_uri->query, *key); return APR_SUCCESS; } + +apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, + const char **key) +{ + return cache_canonicalise_key(r, p, r->uri, &r->parsed_uri, key); +} + +/* + * Invalidate a specific URL entity in all caches + * + * All cached entities for this URL are removed, usually in + * response to a POST/PUT or DELETE. + * + * This function returns OK if at least one entity was found and + * removed, and DECLINED if no cached entities were removed. + */ +int cache_invalidate(cache_request_rec *cache, request_rec *r) +{ + cache_provider_list *list; + apr_status_t rv, status = DECLINED; + cache_handle_t *h; + apr_uri_t location_uri; + apr_uri_t content_location_uri; + + const char *location, *location_key = NULL; + const char *content_location, *content_location_key = NULL; + + if (!cache) { + /* This should never happen */ + ap_log_rerror( + APLOG_MARK, APLOG_ERR, APR_EGENERAL, r, APLOGNO(00697) "cache: No cache request information available for key" + " generation"); + return DECLINED; + } + + if (!cache->key) { + rv = cache_generate_key(r, r->pool, &cache->key); + if (rv != APR_SUCCESS) { + return DECLINED; + } + } + + location = apr_table_get(r->headers_out, "Location"); + if (location) { + if (APR_SUCCESS != apr_uri_parse(r->pool, location, &location_uri) + || APR_SUCCESS + != cache_canonicalise_key(r, r->pool, location, + &location_uri, &location_key) + || strcmp(r->parsed_uri.hostname, location_uri.hostname)) { + location_key = NULL; + } + } + + content_location = apr_table_get(r->headers_out, "Content-Location"); + if (content_location) { + if (APR_SUCCESS + != apr_uri_parse(r->pool, content_location, + &content_location_uri) + || APR_SUCCESS + != cache_canonicalise_key(r, r->pool, content_location, + &content_location_uri, &content_location_key) + || strcmp(r->parsed_uri.hostname, + content_location_uri.hostname)) { + content_location_key = NULL; + } + } + + /* go through the cache types */ + h = apr_palloc(r->pool, sizeof(cache_handle_t)); + + list = cache->providers; + + while (list) { + + /* invalidate the request uri */ + rv = list->provider->open_entity(h, r, cache->key); + if (OK == rv) { + rv = list->provider->invalidate_entity(h, r); + status = OK; + } + ap_log_rerror( + APLOG_MARK, APLOG_DEBUG, rv, r, APLOGNO(02468) "cache: Attempted to invalidate cached entity with key: %s", cache->key); + + /* invalidate the Location */ + if (location_key) { + rv = list->provider->open_entity(h, r, location_key); + if (OK == rv) { + rv = list->provider->invalidate_entity(h, r); + status = OK; + } + ap_log_rerror( + APLOG_MARK, APLOG_DEBUG, rv, r, APLOGNO(02469) "cache: Attempted to invalidate cached entity with key: %s", location_key); + } + + /* invalidate the Content-Location */ + if (content_location_key) { + rv = list->provider->open_entity(h, r, content_location_key); + if (OK == rv) { + rv = list->provider->invalidate_entity(h, r); + status = OK; + } + ap_log_rerror( + APLOG_MARK, APLOG_DEBUG, rv, r, APLOGNO(02470) "cache: Attempted to invalidate cached entity with key: %s", content_location_key); + } + + list = list->next; + } + + return status; +} diff --git a/modules/cache/cache_storage.h b/modules/cache/cache_storage.h index 2b67970e1e..90445f0655 100644 --- a/modules/cache/cache_storage.h +++ b/modules/cache/cache_storage.h @@ -40,6 +40,20 @@ int cache_remove_url(cache_request_rec *cache, request_rec *r); int cache_create_entity(cache_request_rec *cache, request_rec *r, apr_off_t size, apr_bucket_brigade *in); int cache_select(cache_request_rec *cache, request_rec *r); + +/** + * invalidate a specific URL entity in all caches + * + * All cached entities for this URL are removed, usually in + * response to a POST/PUT or DELETE. + * + * This function returns OK if at least one entity was found and + * removed, and DECLINED if no cached entities were removed. + * @param h cache_handle_t + * @param r request_rec + */ +int cache_invalidate(cache_request_rec *cache, request_rec *r); + apr_status_t cache_generate_key_default(request_rec *r, apr_pool_t* p, const char **key); diff --git a/modules/cache/cache_util.c b/modules/cache/cache_util.c index 1e5098d537..cdabe8ff82 100644 --- a/modules/cache/cache_util.c +++ b/modules/cache/cache_util.c @@ -544,11 +544,13 @@ int cache_check_freshness(cache_handle_t *h, cache_request_rec *cache, /* These come from the cached entity. */ if (h->cache_obj->info.control.no_cache || h->cache_obj->info.control.no_cache_header - || h->cache_obj->info.control.private_header) { + || h->cache_obj->info.control.private_header + || h->cache_obj->info.control.invalidated) { /* * The cached entity contained Cache-Control: no-cache, or a * no-cache with a header present, or a private with a header - * present, so treat as stale causing revalidation. + * present, or the cached entity has been invalidated in the + * past, so treat as stale causing revalidation. */ return 0; } diff --git a/modules/cache/mod_cache.c b/modules/cache/mod_cache.c index b1bb65abd0..6a0ca00785 100644 --- a/modules/cache/mod_cache.c +++ b/modules/cache/mod_cache.c @@ -34,6 +34,7 @@ static ap_filter_rec_t *cache_save_subreq_filter_handle; static ap_filter_rec_t *cache_out_filter_handle; static ap_filter_rec_t *cache_out_subreq_filter_handle; static ap_filter_rec_t *cache_remove_url_filter_handle; +static ap_filter_rec_t *cache_invalidate_filter_handle; /* * CACHE handler @@ -75,11 +76,6 @@ static int cache_quick_handler(request_rec *r, int lookup) ap_filter_rec_t *cache_out_handle; cache_server_conf *conf; - /* Delay initialization until we know we are handling a GET */ - if (r->method_number != M_GET) { - return DECLINED; - } - conf = (cache_server_conf *) ap_get_module_config(r->server->module_config, &cache_module); @@ -117,6 +113,40 @@ static int cache_quick_handler(request_rec *r, int lookup) return DECLINED; } + /* Are we PUT/POST/DELETE? If so, prepare to invalidate the cached entities. + */ + switch (r->method_number) { + case M_PUT: + case M_POST: + case M_DELETE: + { + + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, APLOGNO(02461) + "PUT/POST/DELETE: Adding CACHE_INVALIDATE filter for %s", + r->uri); + + /* Add cache_invalidate filter to this request to force a + * cache entry to be invalidated if the response is + * ultimately successful (2xx). + */ + ap_add_output_filter_handle( + cache_invalidate_filter_handle, cache, r, + r->connection); + + return DECLINED; + } + case M_GET: { + break; + } + default : { + + ap_log_rerror( + APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, APLOGNO(02462) "cache: Method '%s' not cacheable by mod_cache, ignoring: %s", r->method, r->uri); + + return DECLINED; + } + } + /* * Try to serve this request from the cache. * @@ -176,9 +206,10 @@ static int cache_quick_handler(request_rec *r, int lookup) * is available later during running the filter may be * different due to an internal redirect. */ - cache->remove_url_filter = - ap_add_output_filter_handle(cache_remove_url_filter_handle, - cache, r, r->connection); + cache->remove_url_filter = ap_add_output_filter_handle( + cache_remove_url_filter_handle, cache, r, + r->connection); + } else { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, @@ -347,11 +378,6 @@ static int cache_handler(request_rec *r) ap_filter_rec_t *cache_save_handle; cache_server_conf *conf; - /* Delay initialization until we know we are handling a GET */ - if (r->method_number != M_GET) { - return DECLINED; - } - conf = (cache_server_conf *) ap_get_module_config(r->server->module_config, &cache_module); @@ -375,6 +401,40 @@ static int cache_handler(request_rec *r) /* save away the possible providers */ cache->providers = providers; + /* Are we PUT/POST/DELETE? If so, prepare to invalidate the cached entities. + */ + switch (r->method_number) { + case M_PUT: + case M_POST: + case M_DELETE: + { + + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, APLOGNO(02463) + "PUT/POST/DELETE: Adding CACHE_INVALIDATE filter for %s", + r->uri); + + /* Add cache_invalidate filter to this request to force a + * cache entry to be invalidated if the response is + * ultimately successful (2xx). + */ + ap_add_output_filter_handle( + cache_invalidate_filter_handle, cache, r, + r->connection); + + return DECLINED; + } + case M_GET: { + break; + } + default : { + + ap_log_rerror( + APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, APLOGNO(02464) "cache: Method '%s' not cacheable by mod_cache, ignoring: %s", r->method, r->uri); + + return DECLINED; + } + } + /* * Try to serve this request from the cache. * @@ -455,9 +515,10 @@ static int cache_handler(request_rec *r) * is available later during running the filter may be * different due to an internal redirect. */ - cache->remove_url_filter = - ap_add_output_filter_handle(cache_remove_url_filter_handle, - cache, r, r->connection); + cache->remove_url_filter + = ap_add_output_filter_handle( + cache_remove_url_filter_handle, cache, r, + r->connection); } else { @@ -1482,6 +1543,70 @@ static apr_status_t cache_remove_url_filter(ap_filter_t *f, return ap_pass_brigade(f->next, in); } +/* + * CACHE_INVALIDATE filter + * ----------------------- + * + * This filter gets added in the quick handler should a PUT, POST or DELETE + * method be detected. If the response is successful, we must invalidate any + * cached entity as per RFC2616 section 13.10. + * + * CACHE_INVALIDATE has to be a protocol filter to ensure that is run even if + * the response is a canned error message, which removes the content filters + * from the chain. + * + * CACHE_INVALIDATE expects cache request rec within its context because the + * request this filter runs on can be different from the one whose cache entry + * should be removed, due to internal redirects. + */ +static apr_status_t cache_invalidate_filter(ap_filter_t *f, + apr_bucket_brigade *in) +{ + request_rec *r = f->r; + cache_request_rec *cache; + + /* Setup cache_request_rec */ + cache = (cache_request_rec *) f->ctx; + + if (!cache) { + /* user likely configured CACHE_INVALIDATE manually; they should really + * use mod_cache configuration to do that. So: + * 1. Remove ourselves + * 2. Do nothing and bail out + */ + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02465) + "cache: CACHE_INVALIDATE enabled unexpectedly: %s", r->uri); + } + else { + + if (r->status > 299) { + + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02466) + "cache: response status to '%s' method is %d (>299), not invalidating cached entity: %s", r->method, r->status, r->uri); + + } + else { + + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, APLOGNO(02467) + "cache: Invalidating all cached entities in response to '%s' request for %s", + r->method, r->uri); + + cache_invalidate(cache, r); + + /* we've got a cache invalidate! tell everyone who cares */ + cache_run_cache_status(cache->handle, r, r->headers_out, + AP_CACHE_INVALIDATE, apr_psprintf(r->pool, + "cache invalidated by %s", r->method)); + + } + + } + + /* remove ourselves */ + ap_remove_output_filter(f); + return ap_pass_brigade(f->next, in); +} + /* * CACHE filter * ------------ @@ -1579,11 +1704,11 @@ static int cache_status(cache_handle_t *h, request_rec *r, x_cache = conf->x_cache; } if (x_cache) { - apr_table_setn(headers, "X-Cache", - apr_psprintf(r->pool, "%s from %s", - status == AP_CACHE_HIT ? "HIT" : status - == AP_CACHE_REVALIDATE ? "REVALIDATE" : "MISS", - r->server->server_hostname)); + apr_table_setn(headers, "X-Cache", apr_psprintf(r->pool, "%s from %s", + status == AP_CACHE_HIT ? "HIT" + : status == AP_CACHE_REVALIDATE ? "REVALIDATE" : status + == AP_CACHE_INVALIDATE ? "INVALIDATE" : "MISS", + r->server->server_hostname)); } if (dconf && dconf->x_cache_detail_set) { @@ -2455,6 +2580,11 @@ static void register_hooks(apr_pool_t *p) cache_remove_url_filter, NULL, AP_FTYPE_PROTOCOL); + cache_invalidate_filter_handle = + ap_register_output_filter("CACHE_INVALIDATE", + cache_invalidate_filter, + NULL, + AP_FTYPE_PROTOCOL); ap_hook_post_config(cache_post_config, NULL, NULL, APR_HOOK_REALLY_FIRST); } diff --git a/modules/cache/mod_cache_disk.c b/modules/cache/mod_cache_disk.c index 670e4b8d0c..b0d86fe1ff 100644 --- a/modules/cache/mod_cache_disk.c +++ b/modules/cache/mod_cache_disk.c @@ -1342,7 +1342,17 @@ static apr_status_t commit_entity(cache_handle_t *h, request_rec *r) static apr_status_t invalidate_entity(cache_handle_t *h, request_rec *r) { - return APR_ENOTIMPL; + apr_status_t rv; + + rv = recall_headers(h, r); + if (rv != APR_SUCCESS) { + return rv; + } + + /* mark the entity as invalidated */ + h->cache_obj->info.control.invalidated = 1; + + return commit_entity(h, r); } static void *create_dir_config(apr_pool_t *p, char *dummy) diff --git a/modules/cache/mod_cache_socache.c b/modules/cache/mod_cache_socache.c index 9632db123e..fb3fea948a 100644 --- a/modules/cache/mod_cache_socache.c +++ b/modules/cache/mod_cache_socache.c @@ -1182,7 +1182,10 @@ fail: static apr_status_t invalidate_entity(cache_handle_t *h, request_rec *r) { - return APR_ENOTIMPL; + /* mark the entity as invalidated */ + h->cache_obj->info.control.invalidated = 1; + + return commit_entity(h, r); } static void *create_dir_config(apr_pool_t *p, char *dummy) -- 2.40.0