Home Reference Source

packages/skygear-core/lib/cloud/transport/http.js

/**
 * Copyright 2015 Oursky Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import http from 'http';

import registry from '../registry';
import {
  settings
} from '../settings';
import { SkygearError } from '../../error';
import CommonTransport from './common';
import { createLogger } from '../logging';


class HTTPTransport extends CommonTransport {
  constructor(reg) {
    super(reg);

    this.registry = reg;
    this.dispatch = this.dispatch.bind(this);
    this.readReq = this.readReq.bind(this);
    this.server = null;
    this.logger = createLogger('plugin').child({tag: 'plugin'});
  }

  start() {
    if (this.server !== null) {
      throw new Error('HTTPTransport can only start once.');
    }
    this.server = http.createServer(this.readReq);
    this.server.on('clientError', (err, socket) => {
      socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
    });
    this.server.on('error', (err) => {
      this.logger.error(err);
    });

    const {
      address,
      port
    } = settings.http;

    this.logger.info(`Listening ${address} on port ${port}...`);
    this.server.listen(port, address);
  }

  close() {
    this.server.close();
    this.server = null;
  }

  readReq(req, res) {
    req.setEncoding('utf8');
    let body = '';
    req.on('data', (chunk) => {
      body += chunk.toString();
    });
    req.on('end', () => {
      let data;
      try {
        data = JSON.parse(body);
      } catch (e) {
        this.writeError(res, 400, 'Invalid request body', e);
        return;
      }
      try {
        this.dispatch(data, res);
      } catch (e) {
        this.writeError(res, 500, 'Internal server error', e);
        this.logger.warn({err: e});
        return;
      }
    });
  }

  async dispatch(payload, res) {
    const handlerName = payload.kind + 'Handler';
    if (!this[handlerName]) {
      this.writeError(res, 400, `func kind ${payload.kind} is not supported`);
      this.logger.log(`func kind ${payload.kind} is not supported`);
      return;
    }

    const {
      context
    } = payload;

    const logger = createLogger('plugin', context);

    try {
      const resolved = await this[handlerName](payload);
      this.writeResponse(res, resolved);
    } catch (err) {
      let responseError = err;
      if (err instanceof SkygearError) {
        // do nothing
      } else if (err !== null && err !== undefined) {
        logger.error({err: err}, 'Catching unexpected error: %s', err);
        responseError = new SkygearError(err.toString());
      } else {
        logger.error('Catching err but value is null or undefined.');
        responseError = new SkygearError('An unexpected error has occurred.');
      }
      this.writeResponse(res, {
        error: responseError.toJSON()
      });
    }
  }

  writeResponse(res, result) {
    res.writeHead(200, {
      'Content-Type': 'application/json'
    });
    res.write(JSON.stringify(result));
    res.end();
  }

  writeError(res, code, message, error) {
    res.writeHead(code, {
      'Content-Type': 'application/json'
    });
    res.write(error ? `${message}\r\n${error}` : message);
    res.end();
  }
}

const transport = new HTTPTransport(registry);

export default transport;