Skip to content

remscodes/drino

Repository files navigation

Drino

Modern and Reactive HTTP Client

github ci codecov coverage npm version bundle size license

Installation

npm install drino

Table of contents

Basic Usage

Example

import drino from 'drino';

// With Observer's callbacks
drino.get('/cat/meow').consume({
  result: (res) => {
    // handle result 
  },
  error: (err) => {
    // handle error
  },
  finish: () => {
    // after result or error
  }
});

// With Promise async/await
async function getCatInfo() {
  try {
    const res = await drino.get('/cat/meow').consume();
    // handle result 
  }
  catch (err) {
    // handle error
  }
  finally {
    // after result or error
  }
}

Request Methods

drino.get(url, config?)

drino.head(url, config?)

drino.delete(url, config?)

drino.post(url, body, config?)

drino.put(url, body, config?)

drino.patch(url, body, config?)

Request Config

interface RequestConfig {
  // Prefix URL
  // Example : 'https://example.com' OR '/api'
  prefix?: string;

  // HTTP Headers
  headers?: Headers | Record<string, any>;

  // HTTP Parameters
  queryParams?: URLSearchParams | Record<string, any>;

  // Response type that will be passed into :
  // - `result` callback when using Observer
  // - `then` callback when using Promise
  // 
  // If 'auto' is specified, the response type will be inferred from "content-type" response header.
  // 
  // default: 'auto'
  read?: 'auto' | 'object' | 'string' | 'blob' | 'arrayBuffer' | 'formData' | 'none';

  // Wrap response body into a specific Object.
  // - 'response' : HttpResponse
  // - 'none' : nothing
  //
  // default: 'none'
  wrapper?: 'none' | 'response';

  // AbortSignal to cancel HTTP Request with an AbortController.
  // See below in section 'Abort Request'.
  signal?: AbortSignal;

  // Time limit from which the request is aborted.
  //
  // default: 0 (= meaning disabled)
  //
  // See below in section 'Timeout'.
  timeoutMs?: number;

  // Retry a failed request a certain number of times on a specific http status.
  // See below in section 'Request retry'.
  retry?: RetryConfig;

  // Config to inspect download progress.
  // See below in section 'Progress capturing'.
  progress?: ProgressConfig;
}

Instance

Instance can be created to embded common configuration to all requests produced from this instance.

import drino from 'drino';

const instance = drino.create({
  baseUrl: 'http://localhost:8080'
});

instance.get('/cat/meow').consume() // GET -> http://localhost:8080/cat/meow

You can create another instance from a parent instance to inherit its config by using child method :

const child = instance.child({
  requestsConfig: {
    prefix: '/cat'
  }
});

child.get('/meow').consume() // GET -> http://localhost:8080/cat/meow

Instance Config

interface DrinoDefaultConfig {
  // Base URL
  // Example : 'https://example.com/v1/api'
  //
  // default: 'http://localhost'
  baseUrl?: string | URL;

  // Interceptors in order to take action during http request lifecyle.
  //
  // See below in section 'Interceptors'
  interceptors?: {
    beforeConsume?: (req: HttpRequest) => void;
    afterConsume?: (req: HttpRequest) => void;
    beforeResult?: (res: any) => void;
    beforeError?: (errRes: HttpErrorResponse) => void;
    beforeFinish?: () => void;
  };

  // Default requestConfig applied to all requests hosted by the instance
  // See above in section 'Request Config'
  requestsConfig?: RequestConfig;
}

You can override config applied to a drino instance (default import or created instance).

drino.default.baseUrl = 'https://example.com';
drino.default.requestsConfig.headers.set('Custom-Header', 'Cat');

drino.get('/cat/meow').consume(); // GET -> https://example.com/cat/meow (headers = { "Custom-Header", "Cat" })

Plugin

You can use third-party plugin to add more features.

drino.use(myPlugin);

Plugin example : drino-rx

Advanced Usage

Interceptors

You can intercept request, result or error throughout the http request lifecycle.

Interceptors can be passed into instance config .

const instance = drino.create({
  interceptors: {
    // ...
  }
});

Before consume

Intercept a HttpRequest before the request is launched.

Example :

const instance = drino.create({
  interceptors: {
    beforeConsume: (req) => {
      const token = myService.getToken();
      req.headers.set('Authorization', `Bearer ${token}`);
    }
  }
});

After consume

Intercept a HttpRequest just after the response has been received.

Example :

const instance = drino.create({
  interceptors: {
    afterConsume: (req) => {
      console.info(`Response received from ${req.url}`);
    }
  }
});

Before result

Intercept a result before being passed into result callback (Observer) or into then() arg callback (Promise).

Example :

const instance = drino.create({
  interceptors: {
    beforeResult: (res) => {
      console.info(`Result : ${res}`);
    }
  }
});

Before error

Intercept an error before being passed into error callback (Observer) or into catch() arg callback (Promise).

Example :

const instance = drino.create({
  interceptors: {
    beforeError: (errorResponse) => {
      if (errorResponse.status === 401) {
        myService.clearToken();
        myService.navigateToLogin();
      }
      else {
        console.error(`Error ${errorResponse.status} from ${errorResponse.url} : ${errorResponse.error}`);
      }
    }
  }
});

Before finish

Intercept before being passed into finish callback (Observer) or into finally() arg callback (Promise).

Example :

const instance = drino.create({
  interceptors: {
    beforeFinish: () => {
      console.info('Finished');
    }
  }
});

Progress Capturing

Download

You can inspect download progress with downloadProgress observer's callback.

Progress capturing can be disabled for the instance or for the request by set inspect: false into ProgressConfig in RequestConfig.

interface ProgressConfig {
  download?: {
    // Enable download progress.
    //
    // default : true
    inspect?: boolean;
  };
}

A StreamProgressEvent is passed to downloadProgress callback for each progress iteration.

export interface StreamProgressEvent {
  // Total bytes to be received or to be sent;
  total: number;

  // Total bytes received or sent.
  loaded: number;

  // Current percentage received or sent.
  // Between 0 and 1.
  percent: number;

  // Current speed in bytes/ms.
  // Equals to `0` for the first `iteration`.
  speed: number;

  // Estimated remaining time in milliseconds to complete the progress.
  // Equals to `0` for the first `iteration`.
  remainingMs: number;

  // Current chunk received or sent.
  chunk: Uint8Array;

  // Current iteration number of the progress.
  iteration: number;
}

Example :

drino.get('/cat/image').consume({
  downloadProgress: ({ loaded, total, percent, speed, remainingTimeMs }) => {
    const remainingSeconds = remainingTimeMs / 1000;
    const speedKBs = speed / 1024 * 1000;

    console.info(`Received ${loaded} of ${total} bytes (${Math.floor(percent * 100)} %).`);
    console.info(`Speed ${speedKBs.toFixed(1)} KB/s | ${remainingSeconds.toFixed(2)} seconds remaining.`);

    if (loaded === total) console.info('Download completed.');
  },
  result: (res) => {
    // handle result
  }
});

Pipe Methods

Before calling consume() method, you can chain call methods to modify or inspect the current value before being passed into final callbacks.

Transform

Change the result value.

Example :

drino.get('/cat/meow')
  .transform((res) => res.name)
  .consume({
    result: (name) => {
      // handle value
    },
  });

Check

Read the result value without changing it.

Example :

drino.get('/cat/meow')
  .check((res) => console.log(res)) // { name: "Gaïa" }
  .consume({
    result: (res) => {
      // handle value
    }
  });

Report

Read the error value without changing it.

Example :

drino.get('/cat/meow')
  .report((err) => console.error(err.name)) // "ErrorName" 
  .consume({
    result: (res) => {
      // handle value
    }
  });

Finalize

Finalize when controller finished.

Example :

drino.get('/cat/meow')
  .finalize(() => console.log('Finished')) // "Finished"
  .consume({
    result: (res) => {
      // handle value
    }
  });

Follow

Make another http request sequentially that depends on previous one.

Example :

drino.get('/cat/meow')
  .follow((cat) => drino.get(`/dog/wouaf/cat-friend/${cat.name}`))
  .consume({
    result: (res) => {
      // handle value
    }
  });

Methods combination

Pipe methods can be combined.

Example :

drino.get('/cat/meow')
  .check((cat) => console.log(cat)) // { name: "Gaïa" }
  .transform((cat) => cat.name)
  .check((name) => console.log(name)) // "Gaïa"
  .consume({
    result: (name) => {
      // handle value
    }
  });

Request Annulation

AbortController

You can cancel a send request (before receive response) by using AbortSignal and AbortController.

Example :

const controller = new AbortController();
const signal = controller.signal;

setTimeout(() => controller.abort('Too Long'), 2_000);

// With Observer
drino.get('/cat/meow', { signal }).consume({
  result: (res) => {
    // handle result
  },
  abort: (reason) => {
    console.error(reason); // "Too Long"
    // handle abort reason
  }
});

// With Promise async/await
async function getCatInfo() {
  try {
    const result = await drino.get('/cat/meow', { signal }).consume();
    // handle result
  }
  catch (err) {
    if (signal.aborted) {
      const reason = signal.reason;
      console.error(reason); // "Too Long"
      // handle abort reason
    }
  }
}

Timeout

You can cancel a send request after a certain time using a timeoutMs (timeout in milliseconds).

Example :

// With Observer
drino.get('/cat/meow', { timeoutMs: 2_000 }).consume({
  result: (res) => {
    // handle result
  },
  error: (err) => {
    console.error(err.message); // "The operation timed out."
    // handle timeout error
  },
});

// With Promise async/await
async function getCatInfo() {
  try {
    const res = await drino.get('/cat/meow', { timeoutMs: 2_000 }).consume();
    // handle result
  }
  catch (err) {
    const message = err.message;
    console.error(message); // "The operation timed out."
    // handle timeout error
  }
}

Request Retry

You can automatically retry failed request on conditions.

interface RetryConfig {
  // Maximum retries to do on failed request.
  //
  // default: 0
  max: number;

  // Use the "Retry-After" response Header to know how much time it waits before retry.
  //
  // default: true
  withRetryAfter?: boolean;

  // Specify the time in millisecond to wait before retry.
  //
  // Work only if `withRetryAfter` is `false` or if "Retry-After" response header is not present.
  //
  // default: 0
  withDelayMs?: number;

  // HTTP response status code to filter which request should be retried on failure.
  //
  // default: [408, 429, 503, 504]
  onStatus?: number[] | { start: number, end: number } | { start: number, end: number }[];

  // Http method to filter which request should be retried on failure.
  // Can only be used for instance configuration.
  // 
  // "*" means all methods.
  //
  // Example: ["GET", "POST"]
  // 
  // default: "*"
  onMethods?: '*' | string[];
}

Example :

const instance = drino.create({
  requestsConfig: {
    retry: { max: 2, onMethods: ['GET'] }
  }
});

instance.get('/my-failed-api', {
  retry: { max: 1 }
});

When using Observer you can use the retry callback to get info about current retry via RetryEvent.

export interface RetryEvent {
  // Current retry count.
  count: number;

  // Error that causes the retry.
  error: any;

  // Function to abort retrying.
  abort: (reason?: any) => void;

  // Current retry delay.
  delay: number;
}

Example :

instance.get('/my-failed-api').consume({
  retry: ({ count, error, abort }) => {
    console.log(`Will retry for the ${count} time caused by the error : ${error}.`);
    if (count > 2) abort('Too many retries.');
  },
  abort: (reason) => {
    console.log(reason); // "Too many retries."
  }
});

React Native support

Install react-native-url-polyfill and add the following line at the top of your index.js file :

import 'react-native-url-polyfill/auto';

License

MIT © Rémy Abitbol.