conditional-request-middleware

deno land deno doc GitHub release (latest by date) codecov GitHub

test NPM

HTTP conditional request middleware.

Compliant with RFC 9110, 13. Conditional Requests

Middleware

For a definition of Universal HTTP middleware, see the http-middleware project.

Usage

To evaluate precondition, you need to provide select representation function to retrieve the selected representation.

The following example evaluates the If-None-Match precondition and handle response.

import {
  conditionalRequest,
  type Handler,
} from "https://deno.land/x/conditional_request_middleware@$VERSION/mod.ts";
import {
  assertEquals,
  assertFalse,
} from "https://deno.land/std/testing/asserts.ts";
import { assertSpyCalls, spy } from "https://deno.land/std/testing/mock.ts";

const selectRepresentation = spy((request: Request) => {
  return new Response("<body>", { headers: { etag: "<etag>" } });
});
const middleware = conditionalRequest(selectRepresentation);
const request = new Request("<uri>", {
  headers: { "if-none-match": "<etag>" },
});
declare const _handler: Handler;
const handler = spy(_handler);

const response = await middleware(request, handler);

assertSpyCalls(handler, 0);
assertSpyCalls(selectRepresentation, 1);
assertEquals(response.status, 304);
assertFalse(response.body);

Select representation

The evaluation of the pre-conditions is always done on the selected representation.

You must provide a function to retrieve the representation.

Selecting representation is the following interface:

interface SelectRepresentation {
  (request: Request): Response | Promise<Response>;
}

It is executed prior to the handler when a request with a precondition header is received.

The Request object passed to select representation has fileted the conditional header.

It satisfies the following requirement.

A server MUST ignore all received preconditions if its response to the same request without those conditions, prior to processing the request content, would have been a status code other than a 2xx (Successful) or 412 (Precondition Failed).

Preconditions

Middleware supports all preconditions compliant with RFC 9110, 13.1. Preconditions by default.

If you want to adapt only some of the preconditions, give a list of them.

Example of middleware that handles only If-None-Match and If-Modified-Since headers:

import {
  conditionalRequest,
  type Handler,
  IfModifiedSince,
  IfNoneMatch,
} from "https://deno.land/x/conditional_request_middleware@$VERSION/mod.ts";

declare const selectRepresentation: Handler;

const middleware = conditionalRequest(selectRepresentation, {
  preconditions: [new IfNoneMatch(), new IfModifiedSince()],
});

Don't worry about the order of preconditions. They will be sorted appropriately based on the 13.2.2. Precedence of Preconditions.

Middleware default

The Middleware factory default values are as follows:

import {
  BytesRange,
  conditionalRequest,
  type Handler,
  IfMatch,
  IfModifiedSince,
  IfNoneMatch,
  IfRange,
  IfUnmodifiedSince,
} from "https://deno.land/x/conditional_request_middleware@$VERSION/mod.ts";

declare const selectRepresentation: Handler;
const DEFAULT_PRECONDITIONS = [
  new IfMatch(),
  new IfNoneMatch(),
  new IfModifiedSince(),
  new IfUnmodifiedSince(),
  new IfRange([new BytesRange()]),
];

const middleware = conditionalRequest(selectRepresentation, {
  preconditions: DEFAULT_PRECONDITIONS,
});

Precondition

Precondition is following structured object.

/** Precondition API. */
export interface Precondition {
  /** Precondition header field name. */
  readonly field: string;

  /** Definition of precondition evaluation.
   * If return value is void, it represents ignore this precondition.
   */
  evaluate(
    request: Request,
    selectedRepresentation: Response,
  ): boolean | void | Promise<boolean | void>;

  /** Called after {@link Precondition.evaluate}.
   * If return response, it must not perform the requested method.
   * If return value is void, it represents ignore this precondition.
   */
  respond(
    request: Request,
    selectedRepresentation: Response,
    result: boolean,
  ): Response | void | Promise<Response | void>;
}

Precondition abstracts the evaluation of a precondition and its response.

Provide all preconditions compliant with RFC 9110, 13.1. Preconditions

If you implement a Precondition that is not in the specification, make sure extensibility of preconditions.

IfMatch

If-Match header field precondition.

import { IfMatch } from "https://deno.land/x/conditional_request_middleware@$VERSION/preconditions/if_match.ts";
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

const precondition = new IfMatch();
const request = new Request("<uri>", {
  headers: { "if-match": "<strong:etag>" },
});
const selectedRepresentation = new Response("<content>", {
  headers: { etag: "<weak:etag>" },
});
declare const evalResult: false;

assertEquals(precondition.field, "if-match");
assertEquals(
  precondition.evaluate(request, selectedRepresentation),
  evalResult,
);
assertEquals(
  precondition.respond(request, selectedRepresentation, evalResult)?.status,
  412,
);

Effects

Precondition will effect following:

If evaluation is false:

IfNoneMatch

If-None-Match header field precondition.

import { IfNoneMatch } from "https://deno.land/x/conditional_request_middleware@$VERSION/preconditions/if_none_match.ts";
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

const precondition = new IfNoneMatch();
const request = new Request("<uri>", {
  headers: { "if-none-match": "<weak:etag>" },
});
const selectedRepresentation = new Response("<content>", {
  headers: { etag: "<weak:etag>" },
});
declare const evalResult: false;

assertEquals(precondition.field, "if-none-match");
assertEquals(
  precondition.evaluate(request, selectedRepresentation),
  evalResult,
);
assertEquals(
  precondition.respond(request, selectedRepresentation, evalResult)?.status,
  304,
);

Effects

Precondition will effect following:

If evaluation is false:

IfModifiedSince

If-Modified-Since header field precondition.

import { IfModifiedSince } from "https://deno.land/x/conditional_request_middleware@$VERSION/preconditions/if_modified_since.ts";
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

const precondition = new IfModifiedSince();
const request = new Request("<uri>", {
  headers: { "if-modified-since": "<after:HTTP-date>" },
});
const selectedRepresentation = new Response("<content>", {
  headers: { "last-modified": "<before:HTTP-date>" },
});
declare const evalResult: false;

assertEquals(precondition.field, "if-modified-since");
assertEquals(
  precondition.evaluate(request, selectedRepresentation),
  evalResult,
);
assertEquals(
  precondition.respond(request, selectedRepresentation, evalResult)?.status,
  304,
);

Effects

Precondition will effect following:

If evaluation is false:

  • HTTP content
  • HTTP response status
  • HTTP headers
    • Content-Type
    • Content-Encoding
    • Content-Length
    • Content-Language

IfUnmodifiedSince

If-Unmodified-Since header field precondition.

import { IfUnmodifiedSince } from "https://deno.land/x/conditional_request_middleware@$VERSION/preconditions/if_unmodified_since.ts";
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

const precondition = new IfUnmodifiedSince();
const request = new Request("<uri>", {
  headers: { "if-unmodified-since": "<before:HTTP-date>" },
});
const selectedRepresentation = new Response("<content>", {
  headers: { "last-modified": "<after:HTTP-date>" },
});
declare const evalResult: false;

assertEquals(precondition.field, "if-unmodified-since");
assertEquals(
  precondition.evaluate(request, selectedRepresentation),
  evalResult,
);
assertEquals(
  precondition.respond(request, selectedRepresentation, evalResult)?.status,
  412,
);

Effects

Precondition will effect following:

If evaluation is false:

IfRange

If-Range header field precondition.

import { IfRange } from "https://deno.land/x/conditional_request_middleware@$VERSION/preconditions/if_range.ts";
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

const precondition = new IfRange();
const request = new Request("<uri>", {
  headers: { "if-range": "<strong:etag>", range: "<range-unit>=<range-set>" },
});
const selectedRepresentation = new Response("<content>", {
  headers: { etag: "<strong:etag>" },
});
declare const evalResult: false;

assertEquals(precondition.field, "if-range");
assertEquals(
  precondition.evaluate(request, selectedRepresentation),
  evalResult,
);
assertEquals(
  (await precondition.respond(request, selectedRepresentation, evalResult))
    ?.status,
  206,
);

Effects

Precondition will effect following:

If evaluation is true:

Conditions

Middleware will execute only if the following conditions are met:

  • Request is conditional request
  • Request method is not CONNECT, OPTIONS or TRACE
  • Select representation status is 2xx or 412

License

Copyright © 2023-present httpland.

Released under the MIT license