All files / lib/internal/http httpClient.ts

33.33% Statements 7/21
0% Branches 0/32
0% Functions 0/4
33.33% Lines 7/21

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 1171x 1x 1x             1x 1x                                                                                                     1x               1x                                                                                              
import { dispose, DisposableLike } from "@reactive-js/core/lib/disposable";
import { pipe, identity, Function1 } from "@reactive-js/core/lib/functions";
import {
  ObservableLike,
  fromValue,
  map,
  switchMap,
  concatMap,
} from "@reactive-js/core/lib/observable";
import { isSome } from "@reactive-js/core/lib/option";
import { createRedirectHttpRequest } from "./httpRequest";
import {
  HttpContentEncoding,
  HttpRequest,
  HttpResponse,
  HttpStatusCode,
} from "./interfaces";
 
export const enum HttpClientRequestStatusType {
  Start = 1,
  Progress = 2,
  Completed = 3,
  HeadersReceived = 4,
}
 
export type HttpClientRequestStatusStart = {
  readonly type: HttpClientRequestStatusType.Start;
};
 
export type HttpClientRequestStatusProgress = {
  readonly type: HttpClientRequestStatusType.Progress;
  readonly count: number;
};
 
export type HttpClientRequestStatusComplete = {
  readonly type: HttpClientRequestStatusType.Completed;
};
 
export type HttpClientRequestStatusHeadersReceived<
  TResp extends DisposableLike
> = {
  readonly type: HttpClientRequestStatusType.HeadersReceived;
  readonly response: HttpResponse<TResp>;
};
 
export type HttpClientRequestStatus<TResp extends DisposableLike> =
  | HttpClientRequestStatusStart
  | HttpClientRequestStatusProgress
  | HttpClientRequestStatusComplete
  | HttpClientRequestStatusHeadersReceived<TResp>;
 
export type HttpClientRequest<T> = HttpRequest<T> & {
  readonly acceptedEncodings?: readonly HttpContentEncoding[];
  readonly maxRedirects?: number;
};
 
export type HttpClient<
  THttpRequest extends HttpClientRequest<unknown>,
  TResp extends DisposableLike
> = (req: THttpRequest) => ObservableLike<HttpClientRequestStatus<TResp>>;
 
const redirectCodes = [
  HttpStatusCode.MovedPermanently,
  HttpStatusCode.Found,
  HttpStatusCode.SeeOther,
  HttpStatusCode.TemporaryRedirect,
  HttpStatusCode.PermanentRedirect,
];
 
export const withDefaultBehaviors = <TReq, TResp extends DisposableLike>(
  encodeHttpRequest: Function1<
    HttpClientRequest<TReq>,
    HttpClientRequest<TReq>
  > = identity,
) => (
  httpClient: HttpClient<HttpClientRequest<TReq>, TResp>,
): HttpClient<HttpClientRequest<TReq>, TResp> => {
  const sendRequest = (
    request: HttpClientRequest<TReq>,
  ): ObservableLike<HttpClientRequestStatus<TResp>> =>
    pipe(
      request,
      fromValue(),
      map(encodeHttpRequest),
      switchMap(httpClient),
      concatMap(status => {
        if (status.type === HttpClientRequestStatusType.HeadersReceived) {
          const { response } = status;
          const { location, preferences, statusCode } = response;
          const acceptedEncodings = preferences?.acceptedEncodings ?? [];
          const shouldRedirect =
            redirectCodes.includes(statusCode) &&
            isSome(location) &&
            (request?.maxRedirects ?? 10) > 0;
 
          const newRequest = shouldRedirect
            ? createRedirectHttpRequest(request, response)
            : statusCode === HttpStatusCode.ExpectationFailed
            ? { ...request, expectContinue: false }
            : statusCode === HttpStatusCode.UnsupportedMediaType &&
              acceptedEncodings.length > 0
            ? { ...request, acceptedEncodings }
            : request;
 
          if (request !== newRequest) {
            dispose(response.body);
            return sendRequest(newRequest);
          }
          // Fallthrough
        }
        return fromValue()(status);
      }),
    );
 
  return sendRequest;
};