Home Manual Reference Source

source/http/HttpRequest.js

import HttpResponse from "./HttpResponse"
import {UrlParser} from "./UrlParser"


/**
 * API for performing HTTP requests.
 *
 * @example <caption>Make a POST request</caption>
 * const request = new HttpRequest('http://example.com/api/users/')
 * request.post('Hello world')
 *     .then((response) => {
 *         // Success - response is a HttpResponse object.
 *         console.log(response.toString())
 *         if(response.isSuccess()) {
 *             console.log('Success: ', response.body)
 *         } else if (response.isRedirect) {
 *             console.log('Hmm strange, we got a redirect instead of a 2xx response.')
 *         }
 *     })
 *     .catch((error) => {
 *         // Error - response is an HttpResponse object.
 *         console.error(error.toString())
 *         if(error.response.isRedirect()) {
 *             // Yes - redirect is treated as an error by default.
 *             // you can change this by supplying an extra argument
 *             // to HttpResponse()
 *             console.log('We got a 3xx response!', error.response.body)
 *         } else if(error.response.isClientError()) {
 *             console.log('We got a 4xx response!', error.response.body)
 *         } else if (error.response.isServerError()) {
 *             console.log('We got a 5xx response!', error.response.body)
 *         } else if (error.response.isConnectionRefused()) {
 *             console.log('Connection refused.')
 *         }
 *         // throw error  // You can throw the error as an exception
 *     })
 *
 * @example <caption>Make a GET request with a querystring</caption>
 * const request = new HttpRequest('http://example.com/api/users/')
 * request.urlParser.queryString.set('search', 'doe')
 * request.get()
 *     .then((response) => {
 *         console.log('Success!', response.toString())
 *     })
 *     .catch((error) => {
 *         console.error('Error:', error.toString())
 *     })
 */
export default class HttpRequest {
  /**
   * @param {string} url The URL to request.
   *      If this is supplied, it is passed to
   *      {@link HttpRequest#setUrl}
   */
  constructor(url) {
    this._treatRedirectResponseAsError = true
    this.requestHeaders = new Map()
    this.request = null
    this._urlParser = null
    if(typeof url !== 'undefined') {
      this.setUrl(url)
    }
  }

  /**
   * Create a deep copy of this HttpRequest object.
   *
   * WARNING: This does not copy request headers since those
   * are set on the XMLHttpRequest object, and that object is
   * reset in the copy.
   *
   * @return The copy.
   */
  deepCopy () {
    let copy = Object.assign(Object.create(this), this)
    copy.request = null
    if (this._urlParser !== null) {
      copy._urlParser = this._urlParser.deepCopy()
    }
    copy.requestHeaders = new Map(this.requestHeaders)
    return copy
  }

  /**
   * Get the parsed URL of the request.
   *
   * @returns {UrlParser} The UrlParser for the parsed URL.
   */
  get urlParser() {
    return this._urlParser
  }

  /**
   * Set the URL of the request.
   *
   * @param {String} url The URL.
   */
  setUrl(url) {
    this._urlParser = new UrlParser(url)
  }

  /**
   * Set how we treat 3xx responses.
   *
   * By default they are treated as errors, but you can change
   * this behavior by calling this function.
   *
   * @param {bool} treatRedirectResponseAsError Treat 3xx responses as
   *      errors?
   *
   * @example <caption>Do not treat 3xx responses as error</caption>
   * const request = HttpRequest('http://example.com/api/')
   * request.setTreatRedirectResponseAsError(false)
   */
  setTreatRedirectResponseAsError(treatRedirectResponseAsError) {
    this._treatRedirectResponseAsError = treatRedirectResponseAsError
  }

  _makeXMLHttpRequest () {
    return new XMLHttpRequest()
  }

  /**
   * Send the request.
   *
   * @param method The HTTP method. I.e.: "get", "post", ...
   * @param data Request body data. This is sent through
   *      {@link HttpRequest#makeRequestBody} before it
   *      is sent.*
   * @return {Promise} A Promise.
   *
   *      The resolve function argument is an
   *      an object of whatever {@link HttpRequest#makeResponse}
   *      returns.
   *
   *      The reject function argument is a
   *      {@link HttpResponseError} object created using
   *      {@link HttpResponse#toError}.
   */
  send(method, data) {
    method = method.toUpperCase()
    if(this._urlParser === null) {
      throw new TypeError('Can not call send() without an url.')
    }
    return new Promise((resolve, reject) => {
      this.request = this._makeXMLHttpRequest()
      this.request.open(method, this.urlParser.buildUrl(), true)
      this.setDefaultRequestHeaders(method)
      this._applyRequestHeadersToRequest()
      this.request.onload  = () => this._onComplete(resolve, reject)
      this.request.send(this.makeRequestBody(data))
    })
  }

  /**
   * Shortcut for ``send("get", data)``.
   *
   * @see {@link HttpRequest#send}
   */
  get(data) {
    return this.send('get', data)
  }

  /**
   * Shortcut for ``send("head", data)``.
   *
   * @see {@link HttpRequest#send}
   */
  head(data) {
    return this.send('head', data)
  }

  /**
   * Shortcut for ``send("post", data)``.
   *
   * @see {@link HttpRequest#send}
   */
  post(data) {
    return this.send('post', data)
  }

  /**
   * Shortcut for ``send("put", data)``.
   *
   * @see {@link HttpRequest#send}
   */
  put(data) {
    return this.send('put', data)
  }

  /**
   * Shortcut for ``send("patch", data)``.
   *
   * @see {@link HttpRequest#send}
   */
  patch(data) {
    return this.send('patch', data)
  }

  /**
   * Shortcut for ``send("delete", data)``.
   *
   * Named httpdelete to avoid crash with builtin keyword ``delete``.
   *
   * @see {@link HttpRequest#send}
   */
  httpdelete(data) {
    return this.send('delete', data)
  }

  /**
   * Make request body from the provided data.
   *
   * By default this just returns the provided data,
   * but subclasses can override this to perform automatic
   * conversion.
   *
   * Must return a string.
   */
  makeRequestBody(data) {
    return data
  }

  /**
   * Creates a {@link HttpResponse}.
   * @returns {HttpResponse}
   */
  makeResponse() {
    return new HttpResponse(this.request)
  }

  _applyRequestHeadersToRequest () {
    for (let [header, value] of this.requestHeaders) {
      this.request.setRequestHeader(header, value)
    }
  }

  /**
   * Set a request header.
   *
   * @param header The header name. E.g.: ``"Content-type"``.
   * @param value The header value.
   */
  setRequestHeader(header, value) {
    this.requestHeaders.set(header, value)
  }

  /**
   * Set default request headers.
   *
   * Does nothing by default, but subclasses can override this.
   *
   * @param method The HTTP request method (GET, POST, PUT, ...).
   *      Will always be uppercase.
   */
  setDefaultRequestHeaders(method) {}

  _onComplete(resolve, reject) {
    let response = this.makeResponse()
    let isSuccess = false
    if(this._treatRedirectResponseAsError) {
      isSuccess = response.isSuccess()
    } else {
      isSuccess = response.isSuccess() || response.isRedirect()
    }
    if(isSuccess) {
      resolve(response)
    } else {
      reject(response.toError())
    }
  }
}