[6a3a178] | 1 | 'use strict';
|
---|
| 2 | // rfc7231 6.1
|
---|
| 3 | const statusCodeCacheableByDefault = new Set([
|
---|
| 4 | 200,
|
---|
| 5 | 203,
|
---|
| 6 | 204,
|
---|
| 7 | 206,
|
---|
| 8 | 300,
|
---|
| 9 | 301,
|
---|
| 10 | 404,
|
---|
| 11 | 405,
|
---|
| 12 | 410,
|
---|
| 13 | 414,
|
---|
| 14 | 501,
|
---|
| 15 | ]);
|
---|
| 16 |
|
---|
| 17 | // This implementation does not understand partial responses (206)
|
---|
| 18 | const understoodStatuses = new Set([
|
---|
| 19 | 200,
|
---|
| 20 | 203,
|
---|
| 21 | 204,
|
---|
| 22 | 300,
|
---|
| 23 | 301,
|
---|
| 24 | 302,
|
---|
| 25 | 303,
|
---|
| 26 | 307,
|
---|
| 27 | 308,
|
---|
| 28 | 404,
|
---|
| 29 | 405,
|
---|
| 30 | 410,
|
---|
| 31 | 414,
|
---|
| 32 | 501,
|
---|
| 33 | ]);
|
---|
| 34 |
|
---|
| 35 | const errorStatusCodes = new Set([
|
---|
| 36 | 500,
|
---|
| 37 | 502,
|
---|
| 38 | 503,
|
---|
| 39 | 504,
|
---|
| 40 | ]);
|
---|
| 41 |
|
---|
| 42 | const hopByHopHeaders = {
|
---|
| 43 | date: true, // included, because we add Age update Date
|
---|
| 44 | connection: true,
|
---|
| 45 | 'keep-alive': true,
|
---|
| 46 | 'proxy-authenticate': true,
|
---|
| 47 | 'proxy-authorization': true,
|
---|
| 48 | te: true,
|
---|
| 49 | trailer: true,
|
---|
| 50 | 'transfer-encoding': true,
|
---|
| 51 | upgrade: true,
|
---|
| 52 | };
|
---|
| 53 |
|
---|
| 54 | const excludedFromRevalidationUpdate = {
|
---|
| 55 | // Since the old body is reused, it doesn't make sense to change properties of the body
|
---|
| 56 | 'content-length': true,
|
---|
| 57 | 'content-encoding': true,
|
---|
| 58 | 'transfer-encoding': true,
|
---|
| 59 | 'content-range': true,
|
---|
| 60 | };
|
---|
| 61 |
|
---|
| 62 | function toNumberOrZero(s) {
|
---|
| 63 | const n = parseInt(s, 10);
|
---|
| 64 | return isFinite(n) ? n : 0;
|
---|
| 65 | }
|
---|
| 66 |
|
---|
| 67 | // RFC 5861
|
---|
| 68 | function isErrorResponse(response) {
|
---|
| 69 | // consider undefined response as faulty
|
---|
| 70 | if(!response) {
|
---|
| 71 | return true
|
---|
| 72 | }
|
---|
| 73 | return errorStatusCodes.has(response.status);
|
---|
| 74 | }
|
---|
| 75 |
|
---|
| 76 | function parseCacheControl(header) {
|
---|
| 77 | const cc = {};
|
---|
| 78 | if (!header) return cc;
|
---|
| 79 |
|
---|
| 80 | // TODO: When there is more than one value present for a given directive (e.g., two Expires header fields, multiple Cache-Control: max-age directives),
|
---|
| 81 | // the directive's value is considered invalid. Caches are encouraged to consider responses that have invalid freshness information to be stale
|
---|
| 82 | const parts = header.trim().split(/\s*,\s*/); // TODO: lame parsing
|
---|
| 83 | for (const part of parts) {
|
---|
| 84 | const [k, v] = part.split(/\s*=\s*/, 2);
|
---|
| 85 | cc[k] = v === undefined ? true : v.replace(/^"|"$/g, ''); // TODO: lame unquoting
|
---|
| 86 | }
|
---|
| 87 |
|
---|
| 88 | return cc;
|
---|
| 89 | }
|
---|
| 90 |
|
---|
| 91 | function formatCacheControl(cc) {
|
---|
| 92 | let parts = [];
|
---|
| 93 | for (const k in cc) {
|
---|
| 94 | const v = cc[k];
|
---|
| 95 | parts.push(v === true ? k : k + '=' + v);
|
---|
| 96 | }
|
---|
| 97 | if (!parts.length) {
|
---|
| 98 | return undefined;
|
---|
| 99 | }
|
---|
| 100 | return parts.join(', ');
|
---|
| 101 | }
|
---|
| 102 |
|
---|
| 103 | module.exports = class CachePolicy {
|
---|
| 104 | constructor(
|
---|
| 105 | req,
|
---|
| 106 | res,
|
---|
| 107 | {
|
---|
| 108 | shared,
|
---|
| 109 | cacheHeuristic,
|
---|
| 110 | immutableMinTimeToLive,
|
---|
| 111 | ignoreCargoCult,
|
---|
| 112 | _fromObject,
|
---|
| 113 | } = {}
|
---|
| 114 | ) {
|
---|
| 115 | if (_fromObject) {
|
---|
| 116 | this._fromObject(_fromObject);
|
---|
| 117 | return;
|
---|
| 118 | }
|
---|
| 119 |
|
---|
| 120 | if (!res || !res.headers) {
|
---|
| 121 | throw Error('Response headers missing');
|
---|
| 122 | }
|
---|
| 123 | this._assertRequestHasHeaders(req);
|
---|
| 124 |
|
---|
| 125 | this._responseTime = this.now();
|
---|
| 126 | this._isShared = shared !== false;
|
---|
| 127 | this._cacheHeuristic =
|
---|
| 128 | undefined !== cacheHeuristic ? cacheHeuristic : 0.1; // 10% matches IE
|
---|
| 129 | this._immutableMinTtl =
|
---|
| 130 | undefined !== immutableMinTimeToLive
|
---|
| 131 | ? immutableMinTimeToLive
|
---|
| 132 | : 24 * 3600 * 1000;
|
---|
| 133 |
|
---|
| 134 | this._status = 'status' in res ? res.status : 200;
|
---|
| 135 | this._resHeaders = res.headers;
|
---|
| 136 | this._rescc = parseCacheControl(res.headers['cache-control']);
|
---|
| 137 | this._method = 'method' in req ? req.method : 'GET';
|
---|
| 138 | this._url = req.url;
|
---|
| 139 | this._host = req.headers.host;
|
---|
| 140 | this._noAuthorization = !req.headers.authorization;
|
---|
| 141 | this._reqHeaders = res.headers.vary ? req.headers : null; // Don't keep all request headers if they won't be used
|
---|
| 142 | this._reqcc = parseCacheControl(req.headers['cache-control']);
|
---|
| 143 |
|
---|
| 144 | // Assume that if someone uses legacy, non-standard uncecessary options they don't understand caching,
|
---|
| 145 | // so there's no point stricly adhering to the blindly copy&pasted directives.
|
---|
| 146 | if (
|
---|
| 147 | ignoreCargoCult &&
|
---|
| 148 | 'pre-check' in this._rescc &&
|
---|
| 149 | 'post-check' in this._rescc
|
---|
| 150 | ) {
|
---|
| 151 | delete this._rescc['pre-check'];
|
---|
| 152 | delete this._rescc['post-check'];
|
---|
| 153 | delete this._rescc['no-cache'];
|
---|
| 154 | delete this._rescc['no-store'];
|
---|
| 155 | delete this._rescc['must-revalidate'];
|
---|
| 156 | this._resHeaders = Object.assign({}, this._resHeaders, {
|
---|
| 157 | 'cache-control': formatCacheControl(this._rescc),
|
---|
| 158 | });
|
---|
| 159 | delete this._resHeaders.expires;
|
---|
| 160 | delete this._resHeaders.pragma;
|
---|
| 161 | }
|
---|
| 162 |
|
---|
| 163 | // When the Cache-Control header field is not present in a request, caches MUST consider the no-cache request pragma-directive
|
---|
| 164 | // as having the same effect as if "Cache-Control: no-cache" were present (see Section 5.2.1).
|
---|
| 165 | if (
|
---|
| 166 | res.headers['cache-control'] == null &&
|
---|
| 167 | /no-cache/.test(res.headers.pragma)
|
---|
| 168 | ) {
|
---|
| 169 | this._rescc['no-cache'] = true;
|
---|
| 170 | }
|
---|
| 171 | }
|
---|
| 172 |
|
---|
| 173 | now() {
|
---|
| 174 | return Date.now();
|
---|
| 175 | }
|
---|
| 176 |
|
---|
| 177 | storable() {
|
---|
| 178 | // The "no-store" request directive indicates that a cache MUST NOT store any part of either this request or any response to it.
|
---|
| 179 | return !!(
|
---|
| 180 | !this._reqcc['no-store'] &&
|
---|
| 181 | // A cache MUST NOT store a response to any request, unless:
|
---|
| 182 | // The request method is understood by the cache and defined as being cacheable, and
|
---|
| 183 | ('GET' === this._method ||
|
---|
| 184 | 'HEAD' === this._method ||
|
---|
| 185 | ('POST' === this._method && this._hasExplicitExpiration())) &&
|
---|
| 186 | // the response status code is understood by the cache, and
|
---|
| 187 | understoodStatuses.has(this._status) &&
|
---|
| 188 | // the "no-store" cache directive does not appear in request or response header fields, and
|
---|
| 189 | !this._rescc['no-store'] &&
|
---|
| 190 | // the "private" response directive does not appear in the response, if the cache is shared, and
|
---|
| 191 | (!this._isShared || !this._rescc.private) &&
|
---|
| 192 | // the Authorization header field does not appear in the request, if the cache is shared,
|
---|
| 193 | (!this._isShared ||
|
---|
| 194 | this._noAuthorization ||
|
---|
| 195 | this._allowsStoringAuthenticated()) &&
|
---|
| 196 | // the response either:
|
---|
| 197 | // contains an Expires header field, or
|
---|
| 198 | (this._resHeaders.expires ||
|
---|
| 199 | // contains a max-age response directive, or
|
---|
| 200 | // contains a s-maxage response directive and the cache is shared, or
|
---|
| 201 | // contains a public response directive.
|
---|
| 202 | this._rescc['max-age'] ||
|
---|
| 203 | (this._isShared && this._rescc['s-maxage']) ||
|
---|
| 204 | this._rescc.public ||
|
---|
| 205 | // has a status code that is defined as cacheable by default
|
---|
| 206 | statusCodeCacheableByDefault.has(this._status))
|
---|
| 207 | );
|
---|
| 208 | }
|
---|
| 209 |
|
---|
| 210 | _hasExplicitExpiration() {
|
---|
| 211 | // 4.2.1 Calculating Freshness Lifetime
|
---|
| 212 | return (
|
---|
| 213 | (this._isShared && this._rescc['s-maxage']) ||
|
---|
| 214 | this._rescc['max-age'] ||
|
---|
| 215 | this._resHeaders.expires
|
---|
| 216 | );
|
---|
| 217 | }
|
---|
| 218 |
|
---|
| 219 | _assertRequestHasHeaders(req) {
|
---|
| 220 | if (!req || !req.headers) {
|
---|
| 221 | throw Error('Request headers missing');
|
---|
| 222 | }
|
---|
| 223 | }
|
---|
| 224 |
|
---|
| 225 | satisfiesWithoutRevalidation(req) {
|
---|
| 226 | this._assertRequestHasHeaders(req);
|
---|
| 227 |
|
---|
| 228 | // When presented with a request, a cache MUST NOT reuse a stored response, unless:
|
---|
| 229 | // the presented request does not contain the no-cache pragma (Section 5.4), nor the no-cache cache directive,
|
---|
| 230 | // unless the stored response is successfully validated (Section 4.3), and
|
---|
| 231 | const requestCC = parseCacheControl(req.headers['cache-control']);
|
---|
| 232 | if (requestCC['no-cache'] || /no-cache/.test(req.headers.pragma)) {
|
---|
| 233 | return false;
|
---|
| 234 | }
|
---|
| 235 |
|
---|
| 236 | if (requestCC['max-age'] && this.age() > requestCC['max-age']) {
|
---|
| 237 | return false;
|
---|
| 238 | }
|
---|
| 239 |
|
---|
| 240 | if (
|
---|
| 241 | requestCC['min-fresh'] &&
|
---|
| 242 | this.timeToLive() < 1000 * requestCC['min-fresh']
|
---|
| 243 | ) {
|
---|
| 244 | return false;
|
---|
| 245 | }
|
---|
| 246 |
|
---|
| 247 | // the stored response is either:
|
---|
| 248 | // fresh, or allowed to be served stale
|
---|
| 249 | if (this.stale()) {
|
---|
| 250 | const allowsStale =
|
---|
| 251 | requestCC['max-stale'] &&
|
---|
| 252 | !this._rescc['must-revalidate'] &&
|
---|
| 253 | (true === requestCC['max-stale'] ||
|
---|
| 254 | requestCC['max-stale'] > this.age() - this.maxAge());
|
---|
| 255 | if (!allowsStale) {
|
---|
| 256 | return false;
|
---|
| 257 | }
|
---|
| 258 | }
|
---|
| 259 |
|
---|
| 260 | return this._requestMatches(req, false);
|
---|
| 261 | }
|
---|
| 262 |
|
---|
| 263 | _requestMatches(req, allowHeadMethod) {
|
---|
| 264 | // The presented effective request URI and that of the stored response match, and
|
---|
| 265 | return (
|
---|
| 266 | (!this._url || this._url === req.url) &&
|
---|
| 267 | this._host === req.headers.host &&
|
---|
| 268 | // the request method associated with the stored response allows it to be used for the presented request, and
|
---|
| 269 | (!req.method ||
|
---|
| 270 | this._method === req.method ||
|
---|
| 271 | (allowHeadMethod && 'HEAD' === req.method)) &&
|
---|
| 272 | // selecting header fields nominated by the stored response (if any) match those presented, and
|
---|
| 273 | this._varyMatches(req)
|
---|
| 274 | );
|
---|
| 275 | }
|
---|
| 276 |
|
---|
| 277 | _allowsStoringAuthenticated() {
|
---|
| 278 | // following Cache-Control response directives (Section 5.2.2) have such an effect: must-revalidate, public, and s-maxage.
|
---|
| 279 | return (
|
---|
| 280 | this._rescc['must-revalidate'] ||
|
---|
| 281 | this._rescc.public ||
|
---|
| 282 | this._rescc['s-maxage']
|
---|
| 283 | );
|
---|
| 284 | }
|
---|
| 285 |
|
---|
| 286 | _varyMatches(req) {
|
---|
| 287 | if (!this._resHeaders.vary) {
|
---|
| 288 | return true;
|
---|
| 289 | }
|
---|
| 290 |
|
---|
| 291 | // A Vary header field-value of "*" always fails to match
|
---|
| 292 | if (this._resHeaders.vary === '*') {
|
---|
| 293 | return false;
|
---|
| 294 | }
|
---|
| 295 |
|
---|
| 296 | const fields = this._resHeaders.vary
|
---|
| 297 | .trim()
|
---|
| 298 | .toLowerCase()
|
---|
| 299 | .split(/\s*,\s*/);
|
---|
| 300 | for (const name of fields) {
|
---|
| 301 | if (req.headers[name] !== this._reqHeaders[name]) return false;
|
---|
| 302 | }
|
---|
| 303 | return true;
|
---|
| 304 | }
|
---|
| 305 |
|
---|
| 306 | _copyWithoutHopByHopHeaders(inHeaders) {
|
---|
| 307 | const headers = {};
|
---|
| 308 | for (const name in inHeaders) {
|
---|
| 309 | if (hopByHopHeaders[name]) continue;
|
---|
| 310 | headers[name] = inHeaders[name];
|
---|
| 311 | }
|
---|
| 312 | // 9.1. Connection
|
---|
| 313 | if (inHeaders.connection) {
|
---|
| 314 | const tokens = inHeaders.connection.trim().split(/\s*,\s*/);
|
---|
| 315 | for (const name of tokens) {
|
---|
| 316 | delete headers[name];
|
---|
| 317 | }
|
---|
| 318 | }
|
---|
| 319 | if (headers.warning) {
|
---|
| 320 | const warnings = headers.warning.split(/,/).filter(warning => {
|
---|
| 321 | return !/^\s*1[0-9][0-9]/.test(warning);
|
---|
| 322 | });
|
---|
| 323 | if (!warnings.length) {
|
---|
| 324 | delete headers.warning;
|
---|
| 325 | } else {
|
---|
| 326 | headers.warning = warnings.join(',').trim();
|
---|
| 327 | }
|
---|
| 328 | }
|
---|
| 329 | return headers;
|
---|
| 330 | }
|
---|
| 331 |
|
---|
| 332 | responseHeaders() {
|
---|
| 333 | const headers = this._copyWithoutHopByHopHeaders(this._resHeaders);
|
---|
| 334 | const age = this.age();
|
---|
| 335 |
|
---|
| 336 | // A cache SHOULD generate 113 warning if it heuristically chose a freshness
|
---|
| 337 | // lifetime greater than 24 hours and the response's age is greater than 24 hours.
|
---|
| 338 | if (
|
---|
| 339 | age > 3600 * 24 &&
|
---|
| 340 | !this._hasExplicitExpiration() &&
|
---|
| 341 | this.maxAge() > 3600 * 24
|
---|
| 342 | ) {
|
---|
| 343 | headers.warning =
|
---|
| 344 | (headers.warning ? `${headers.warning}, ` : '') +
|
---|
| 345 | '113 - "rfc7234 5.5.4"';
|
---|
| 346 | }
|
---|
| 347 | headers.age = `${Math.round(age)}`;
|
---|
| 348 | headers.date = new Date(this.now()).toUTCString();
|
---|
| 349 | return headers;
|
---|
| 350 | }
|
---|
| 351 |
|
---|
| 352 | /**
|
---|
| 353 | * Value of the Date response header or current time if Date was invalid
|
---|
| 354 | * @return timestamp
|
---|
| 355 | */
|
---|
| 356 | date() {
|
---|
| 357 | const serverDate = Date.parse(this._resHeaders.date);
|
---|
| 358 | if (isFinite(serverDate)) {
|
---|
| 359 | return serverDate;
|
---|
| 360 | }
|
---|
| 361 | return this._responseTime;
|
---|
| 362 | }
|
---|
| 363 |
|
---|
| 364 | /**
|
---|
| 365 | * Value of the Age header, in seconds, updated for the current time.
|
---|
| 366 | * May be fractional.
|
---|
| 367 | *
|
---|
| 368 | * @return Number
|
---|
| 369 | */
|
---|
| 370 | age() {
|
---|
| 371 | let age = this._ageValue();
|
---|
| 372 |
|
---|
| 373 | const residentTime = (this.now() - this._responseTime) / 1000;
|
---|
| 374 | return age + residentTime;
|
---|
| 375 | }
|
---|
| 376 |
|
---|
| 377 | _ageValue() {
|
---|
| 378 | return toNumberOrZero(this._resHeaders.age);
|
---|
| 379 | }
|
---|
| 380 |
|
---|
| 381 | /**
|
---|
| 382 | * Value of applicable max-age (or heuristic equivalent) in seconds. This counts since response's `Date`.
|
---|
| 383 | *
|
---|
| 384 | * For an up-to-date value, see `timeToLive()`.
|
---|
| 385 | *
|
---|
| 386 | * @return Number
|
---|
| 387 | */
|
---|
| 388 | maxAge() {
|
---|
| 389 | if (!this.storable() || this._rescc['no-cache']) {
|
---|
| 390 | return 0;
|
---|
| 391 | }
|
---|
| 392 |
|
---|
| 393 | // Shared responses with cookies are cacheable according to the RFC, but IMHO it'd be unwise to do so by default
|
---|
| 394 | // so this implementation requires explicit opt-in via public header
|
---|
| 395 | if (
|
---|
| 396 | this._isShared &&
|
---|
| 397 | (this._resHeaders['set-cookie'] &&
|
---|
| 398 | !this._rescc.public &&
|
---|
| 399 | !this._rescc.immutable)
|
---|
| 400 | ) {
|
---|
| 401 | return 0;
|
---|
| 402 | }
|
---|
| 403 |
|
---|
| 404 | if (this._resHeaders.vary === '*') {
|
---|
| 405 | return 0;
|
---|
| 406 | }
|
---|
| 407 |
|
---|
| 408 | if (this._isShared) {
|
---|
| 409 | if (this._rescc['proxy-revalidate']) {
|
---|
| 410 | return 0;
|
---|
| 411 | }
|
---|
| 412 | // if a response includes the s-maxage directive, a shared cache recipient MUST ignore the Expires field.
|
---|
| 413 | if (this._rescc['s-maxage']) {
|
---|
| 414 | return toNumberOrZero(this._rescc['s-maxage']);
|
---|
| 415 | }
|
---|
| 416 | }
|
---|
| 417 |
|
---|
| 418 | // If a response includes a Cache-Control field with the max-age directive, a recipient MUST ignore the Expires field.
|
---|
| 419 | if (this._rescc['max-age']) {
|
---|
| 420 | return toNumberOrZero(this._rescc['max-age']);
|
---|
| 421 | }
|
---|
| 422 |
|
---|
| 423 | const defaultMinTtl = this._rescc.immutable ? this._immutableMinTtl : 0;
|
---|
| 424 |
|
---|
| 425 | const serverDate = this.date();
|
---|
| 426 | if (this._resHeaders.expires) {
|
---|
| 427 | const expires = Date.parse(this._resHeaders.expires);
|
---|
| 428 | // A cache recipient MUST interpret invalid date formats, especially the value "0", as representing a time in the past (i.e., "already expired").
|
---|
| 429 | if (Number.isNaN(expires) || expires < serverDate) {
|
---|
| 430 | return 0;
|
---|
| 431 | }
|
---|
| 432 | return Math.max(defaultMinTtl, (expires - serverDate) / 1000);
|
---|
| 433 | }
|
---|
| 434 |
|
---|
| 435 | if (this._resHeaders['last-modified']) {
|
---|
| 436 | const lastModified = Date.parse(this._resHeaders['last-modified']);
|
---|
| 437 | if (isFinite(lastModified) && serverDate > lastModified) {
|
---|
| 438 | return Math.max(
|
---|
| 439 | defaultMinTtl,
|
---|
| 440 | ((serverDate - lastModified) / 1000) * this._cacheHeuristic
|
---|
| 441 | );
|
---|
| 442 | }
|
---|
| 443 | }
|
---|
| 444 |
|
---|
| 445 | return defaultMinTtl;
|
---|
| 446 | }
|
---|
| 447 |
|
---|
| 448 | timeToLive() {
|
---|
| 449 | const age = this.maxAge() - this.age();
|
---|
| 450 | const staleIfErrorAge = age + toNumberOrZero(this._rescc['stale-if-error']);
|
---|
| 451 | const staleWhileRevalidateAge = age + toNumberOrZero(this._rescc['stale-while-revalidate']);
|
---|
| 452 | return Math.max(0, age, staleIfErrorAge, staleWhileRevalidateAge) * 1000;
|
---|
| 453 | }
|
---|
| 454 |
|
---|
| 455 | stale() {
|
---|
| 456 | return this.maxAge() <= this.age();
|
---|
| 457 | }
|
---|
| 458 |
|
---|
| 459 | _useStaleIfError() {
|
---|
| 460 | return this.maxAge() + toNumberOrZero(this._rescc['stale-if-error']) > this.age();
|
---|
| 461 | }
|
---|
| 462 |
|
---|
| 463 | useStaleWhileRevalidate() {
|
---|
| 464 | return this.maxAge() + toNumberOrZero(this._rescc['stale-while-revalidate']) > this.age();
|
---|
| 465 | }
|
---|
| 466 |
|
---|
| 467 | static fromObject(obj) {
|
---|
| 468 | return new this(undefined, undefined, { _fromObject: obj });
|
---|
| 469 | }
|
---|
| 470 |
|
---|
| 471 | _fromObject(obj) {
|
---|
| 472 | if (this._responseTime) throw Error('Reinitialized');
|
---|
| 473 | if (!obj || obj.v !== 1) throw Error('Invalid serialization');
|
---|
| 474 |
|
---|
| 475 | this._responseTime = obj.t;
|
---|
| 476 | this._isShared = obj.sh;
|
---|
| 477 | this._cacheHeuristic = obj.ch;
|
---|
| 478 | this._immutableMinTtl =
|
---|
| 479 | obj.imm !== undefined ? obj.imm : 24 * 3600 * 1000;
|
---|
| 480 | this._status = obj.st;
|
---|
| 481 | this._resHeaders = obj.resh;
|
---|
| 482 | this._rescc = obj.rescc;
|
---|
| 483 | this._method = obj.m;
|
---|
| 484 | this._url = obj.u;
|
---|
| 485 | this._host = obj.h;
|
---|
| 486 | this._noAuthorization = obj.a;
|
---|
| 487 | this._reqHeaders = obj.reqh;
|
---|
| 488 | this._reqcc = obj.reqcc;
|
---|
| 489 | }
|
---|
| 490 |
|
---|
| 491 | toObject() {
|
---|
| 492 | return {
|
---|
| 493 | v: 1,
|
---|
| 494 | t: this._responseTime,
|
---|
| 495 | sh: this._isShared,
|
---|
| 496 | ch: this._cacheHeuristic,
|
---|
| 497 | imm: this._immutableMinTtl,
|
---|
| 498 | st: this._status,
|
---|
| 499 | resh: this._resHeaders,
|
---|
| 500 | rescc: this._rescc,
|
---|
| 501 | m: this._method,
|
---|
| 502 | u: this._url,
|
---|
| 503 | h: this._host,
|
---|
| 504 | a: this._noAuthorization,
|
---|
| 505 | reqh: this._reqHeaders,
|
---|
| 506 | reqcc: this._reqcc,
|
---|
| 507 | };
|
---|
| 508 | }
|
---|
| 509 |
|
---|
| 510 | /**
|
---|
| 511 | * Headers for sending to the origin server to revalidate stale response.
|
---|
| 512 | * Allows server to return 304 to allow reuse of the previous response.
|
---|
| 513 | *
|
---|
| 514 | * Hop by hop headers are always stripped.
|
---|
| 515 | * Revalidation headers may be added or removed, depending on request.
|
---|
| 516 | */
|
---|
| 517 | revalidationHeaders(incomingReq) {
|
---|
| 518 | this._assertRequestHasHeaders(incomingReq);
|
---|
| 519 | const headers = this._copyWithoutHopByHopHeaders(incomingReq.headers);
|
---|
| 520 |
|
---|
| 521 | // This implementation does not understand range requests
|
---|
| 522 | delete headers['if-range'];
|
---|
| 523 |
|
---|
| 524 | if (!this._requestMatches(incomingReq, true) || !this.storable()) {
|
---|
| 525 | // revalidation allowed via HEAD
|
---|
| 526 | // not for the same resource, or wasn't allowed to be cached anyway
|
---|
| 527 | delete headers['if-none-match'];
|
---|
| 528 | delete headers['if-modified-since'];
|
---|
| 529 | return headers;
|
---|
| 530 | }
|
---|
| 531 |
|
---|
| 532 | /* MUST send that entity-tag in any cache validation request (using If-Match or If-None-Match) if an entity-tag has been provided by the origin server. */
|
---|
| 533 | if (this._resHeaders.etag) {
|
---|
| 534 | headers['if-none-match'] = headers['if-none-match']
|
---|
| 535 | ? `${headers['if-none-match']}, ${this._resHeaders.etag}`
|
---|
| 536 | : this._resHeaders.etag;
|
---|
| 537 | }
|
---|
| 538 |
|
---|
| 539 | // Clients MAY issue simple (non-subrange) GET requests with either weak validators or strong validators. Clients MUST NOT use weak validators in other forms of request.
|
---|
| 540 | const forbidsWeakValidators =
|
---|
| 541 | headers['accept-ranges'] ||
|
---|
| 542 | headers['if-match'] ||
|
---|
| 543 | headers['if-unmodified-since'] ||
|
---|
| 544 | (this._method && this._method != 'GET');
|
---|
| 545 |
|
---|
| 546 | /* SHOULD send the Last-Modified value in non-subrange cache validation requests (using If-Modified-Since) if only a Last-Modified value has been provided by the origin server.
|
---|
| 547 | Note: This implementation does not understand partial responses (206) */
|
---|
| 548 | if (forbidsWeakValidators) {
|
---|
| 549 | delete headers['if-modified-since'];
|
---|
| 550 |
|
---|
| 551 | if (headers['if-none-match']) {
|
---|
| 552 | const etags = headers['if-none-match']
|
---|
| 553 | .split(/,/)
|
---|
| 554 | .filter(etag => {
|
---|
| 555 | return !/^\s*W\//.test(etag);
|
---|
| 556 | });
|
---|
| 557 | if (!etags.length) {
|
---|
| 558 | delete headers['if-none-match'];
|
---|
| 559 | } else {
|
---|
| 560 | headers['if-none-match'] = etags.join(',').trim();
|
---|
| 561 | }
|
---|
| 562 | }
|
---|
| 563 | } else if (
|
---|
| 564 | this._resHeaders['last-modified'] &&
|
---|
| 565 | !headers['if-modified-since']
|
---|
| 566 | ) {
|
---|
| 567 | headers['if-modified-since'] = this._resHeaders['last-modified'];
|
---|
| 568 | }
|
---|
| 569 |
|
---|
| 570 | return headers;
|
---|
| 571 | }
|
---|
| 572 |
|
---|
| 573 | /**
|
---|
| 574 | * Creates new CachePolicy with information combined from the previews response,
|
---|
| 575 | * and the new revalidation response.
|
---|
| 576 | *
|
---|
| 577 | * Returns {policy, modified} where modified is a boolean indicating
|
---|
| 578 | * whether the response body has been modified, and old cached body can't be used.
|
---|
| 579 | *
|
---|
| 580 | * @return {Object} {policy: CachePolicy, modified: Boolean}
|
---|
| 581 | */
|
---|
| 582 | revalidatedPolicy(request, response) {
|
---|
| 583 | this._assertRequestHasHeaders(request);
|
---|
| 584 | if(this._useStaleIfError() && isErrorResponse(response)) { // I consider the revalidation request unsuccessful
|
---|
| 585 | return {
|
---|
| 586 | modified: false,
|
---|
| 587 | matches: false,
|
---|
| 588 | policy: this,
|
---|
| 589 | };
|
---|
| 590 | }
|
---|
| 591 | if (!response || !response.headers) {
|
---|
| 592 | throw Error('Response headers missing');
|
---|
| 593 | }
|
---|
| 594 |
|
---|
| 595 | // These aren't going to be supported exactly, since one CachePolicy object
|
---|
| 596 | // doesn't know about all the other cached objects.
|
---|
| 597 | let matches = false;
|
---|
| 598 | if (response.status !== undefined && response.status != 304) {
|
---|
| 599 | matches = false;
|
---|
| 600 | } else if (
|
---|
| 601 | response.headers.etag &&
|
---|
| 602 | !/^\s*W\//.test(response.headers.etag)
|
---|
| 603 | ) {
|
---|
| 604 | // "All of the stored responses with the same strong validator are selected.
|
---|
| 605 | // If none of the stored responses contain the same strong validator,
|
---|
| 606 | // then the cache MUST NOT use the new response to update any stored responses."
|
---|
| 607 | matches =
|
---|
| 608 | this._resHeaders.etag &&
|
---|
| 609 | this._resHeaders.etag.replace(/^\s*W\//, '') ===
|
---|
| 610 | response.headers.etag;
|
---|
| 611 | } else if (this._resHeaders.etag && response.headers.etag) {
|
---|
| 612 | // "If the new response contains a weak validator and that validator corresponds
|
---|
| 613 | // to one of the cache's stored responses,
|
---|
| 614 | // then the most recent of those matching stored responses is selected for update."
|
---|
| 615 | matches =
|
---|
| 616 | this._resHeaders.etag.replace(/^\s*W\//, '') ===
|
---|
| 617 | response.headers.etag.replace(/^\s*W\//, '');
|
---|
| 618 | } else if (this._resHeaders['last-modified']) {
|
---|
| 619 | matches =
|
---|
| 620 | this._resHeaders['last-modified'] ===
|
---|
| 621 | response.headers['last-modified'];
|
---|
| 622 | } else {
|
---|
| 623 | // If the new response does not include any form of validator (such as in the case where
|
---|
| 624 | // a client generates an If-Modified-Since request from a source other than the Last-Modified
|
---|
| 625 | // response header field), and there is only one stored response, and that stored response also
|
---|
| 626 | // lacks a validator, then that stored response is selected for update.
|
---|
| 627 | if (
|
---|
| 628 | !this._resHeaders.etag &&
|
---|
| 629 | !this._resHeaders['last-modified'] &&
|
---|
| 630 | !response.headers.etag &&
|
---|
| 631 | !response.headers['last-modified']
|
---|
| 632 | ) {
|
---|
| 633 | matches = true;
|
---|
| 634 | }
|
---|
| 635 | }
|
---|
| 636 |
|
---|
| 637 | if (!matches) {
|
---|
| 638 | return {
|
---|
| 639 | policy: new this.constructor(request, response),
|
---|
| 640 | // Client receiving 304 without body, even if it's invalid/mismatched has no option
|
---|
| 641 | // but to reuse a cached body. We don't have a good way to tell clients to do
|
---|
| 642 | // error recovery in such case.
|
---|
| 643 | modified: response.status != 304,
|
---|
| 644 | matches: false,
|
---|
| 645 | };
|
---|
| 646 | }
|
---|
| 647 |
|
---|
| 648 | // use other header fields provided in the 304 (Not Modified) response to replace all instances
|
---|
| 649 | // of the corresponding header fields in the stored response.
|
---|
| 650 | const headers = {};
|
---|
| 651 | for (const k in this._resHeaders) {
|
---|
| 652 | headers[k] =
|
---|
| 653 | k in response.headers && !excludedFromRevalidationUpdate[k]
|
---|
| 654 | ? response.headers[k]
|
---|
| 655 | : this._resHeaders[k];
|
---|
| 656 | }
|
---|
| 657 |
|
---|
| 658 | const newResponse = Object.assign({}, response, {
|
---|
| 659 | status: this._status,
|
---|
| 660 | method: this._method,
|
---|
| 661 | headers,
|
---|
| 662 | });
|
---|
| 663 | return {
|
---|
| 664 | policy: new this.constructor(request, newResponse, {
|
---|
| 665 | shared: this._isShared,
|
---|
| 666 | cacheHeuristic: this._cacheHeuristic,
|
---|
| 667 | immutableMinTimeToLive: this._immutableMinTtl,
|
---|
| 668 | }),
|
---|
| 669 | modified: false,
|
---|
| 670 | matches: true,
|
---|
| 671 | };
|
---|
| 672 | }
|
---|
| 673 | };
|
---|