1 | import _ from 'lodash';
|
2 | import Debug from 'debug';
|
3 | import { IN_BROWSER } from './constants';
|
4 | import { version } from '../package.json';
|
5 | import {
|
6 | CraftAiBadRequestError,
|
7 | CraftAiCredentialsError,
|
8 | CraftAiInternalError,
|
9 | CraftAiLongRequestTimeOutError,
|
10 | CraftAiNetworkError,
|
11 | CraftAiUnknownError
|
12 | } from './errors';
|
13 |
|
14 | const fetch = !IN_BROWSER && typeof fetch === 'undefined'
|
15 | ? require('node-fetch')
|
16 | : window.fetch;
|
17 |
|
18 | const debug = Debug('craft-ai:client');
|
19 |
|
20 | const USER_AGENT = `craft-ai-client-js/${version} [${IN_BROWSER ? navigator.userAgent : `Node.js ${process.version}`}]`;
|
21 |
|
22 | debug(`Client user agent set to '${USER_AGENT}'`);
|
23 |
|
24 | function parseBody(req, resBody) {
|
25 | let resBodyUtf8;
|
26 | try {
|
27 | resBodyUtf8 = resBody.toString('utf-8');
|
28 | }
|
29 | catch (err) {
|
30 | debug(`Invalid response format from ${req.method} ${req.path}: ${resBody}`, err);
|
31 | throw new CraftAiInternalError(
|
32 | 'Internal Error, the craft ai server responded in an invalid format.', {
|
33 | request: req
|
34 | }
|
35 | );
|
36 | }
|
37 | let resBodyJson;
|
38 | try {
|
39 | if (resBodyUtf8.length > 0) {
|
40 | resBodyJson = JSON.parse(resBodyUtf8);
|
41 | }
|
42 | else {
|
43 | resBodyJson = {};
|
44 | }
|
45 | }
|
46 | catch (err) {
|
47 | debug(`Invalid json response from ${req.method} ${req.path}: ${resBody}`, err);
|
48 | throw new CraftAiInternalError(
|
49 | 'Internal Error, the craft ai server responded an invalid json document.', {
|
50 | more: resBodyUtf8,
|
51 | request: req
|
52 | }
|
53 | );
|
54 | }
|
55 | return resBodyJson;
|
56 | }
|
57 |
|
58 | function parseBulk(req, res, resBody) {
|
59 | if (_.isArray(JSON.parse(resBody))) {
|
60 | return { body: parseBody(req, resBody) };
|
61 | }
|
62 | else {
|
63 | throw new CraftAiBadRequestError({
|
64 | message: parseBody(req, resBody).message,
|
65 | request: req
|
66 | });
|
67 | }
|
68 | }
|
69 |
|
70 | function parseResponse(req, res, resBody) {
|
71 | switch (res.status) {
|
72 | case 200:
|
73 | case 201:
|
74 | case 204:
|
75 | return {
|
76 | body: parseBody(req, resBody),
|
77 | nextPageUrl: res.headers.get('x-craft-ai-next-page-url')
|
78 | };
|
79 | case 202:
|
80 | throw new CraftAiLongRequestTimeOutError({
|
81 | message: parseBody(req, resBody).message,
|
82 | request: req
|
83 | });
|
84 | case 207:
|
85 | return parseBulk(req, res, resBody);
|
86 | case 401:
|
87 | case 403:
|
88 | throw new CraftAiCredentialsError({
|
89 | message: parseBody(req, resBody).message,
|
90 | request: req
|
91 | });
|
92 | case 400:
|
93 | case 404:
|
94 | return parseBulk(req, res, resBody);
|
95 | case 413:
|
96 | throw new CraftAiBadRequestError({
|
97 | message: 'Given payload is too large',
|
98 | request: req
|
99 | });
|
100 | case 500:
|
101 | throw new CraftAiInternalError(parseBody(req, resBody).message, {
|
102 | request: req
|
103 | });
|
104 | case 504:
|
105 | throw new CraftAiInternalError({
|
106 | message: 'Response has timed out',
|
107 | request: req,
|
108 | status: res.status
|
109 | });
|
110 | default:
|
111 | throw new CraftAiUnknownError({
|
112 | more: parseBody(req, resBody).message,
|
113 | request: req,
|
114 | status: res.status
|
115 | });
|
116 | }
|
117 | }
|
118 |
|
119 | function createHttpAgent(proxy = undefined) {
|
120 | if (IN_BROWSER) {
|
121 | return undefined;
|
122 | }
|
123 | else if (proxy) {
|
124 | const HttpProxyAgent = require('http-proxy-agent');
|
125 |
|
126 | return new HttpProxyAgent(proxy);
|
127 | }
|
128 | else {
|
129 | const http = require('http');
|
130 |
|
131 | return new http.Agent({
|
132 | keepAlive: true
|
133 | });
|
134 | }
|
135 | }
|
136 |
|
137 | function createHttpsAgent(proxy = undefined) {
|
138 | if (IN_BROWSER) {
|
139 | return undefined;
|
140 | }
|
141 | else if (proxy) {
|
142 | const HttpsProxyAgent = require('https-proxy-agent');
|
143 |
|
144 | return new HttpsProxyAgent(proxy);
|
145 | }
|
146 | else {
|
147 | const https = require('https');
|
148 |
|
149 | return new https.Agent({
|
150 | keepAlive: true
|
151 | });
|
152 | }
|
153 | }
|
154 |
|
155 | export default function createRequest(cfg) {
|
156 | const defaultHeaders = {
|
157 | 'Authorization': `Bearer ${cfg.token}`,
|
158 | 'Content-Type': 'application/json; charset=utf-8',
|
159 | 'Accept': 'application/json'
|
160 | };
|
161 |
|
162 | if (!IN_BROWSER) {
|
163 |
|
164 |
|
165 | defaultHeaders['User-Agent'] = USER_AGENT;
|
166 | }
|
167 |
|
168 | const baseUrl = `${cfg.url}/api/v1/${cfg.owner}/${cfg.project}`;
|
169 |
|
170 | const agent = baseUrl.slice(0, 5) === 'https' ? createHttpsAgent(cfg.proxy) : createHttpAgent(cfg.proxy);
|
171 |
|
172 | return (req) => {
|
173 | req = _.defaults(req || {}, {
|
174 | agent,
|
175 | method: 'GET',
|
176 | path: '',
|
177 | body: undefined,
|
178 | query: {},
|
179 | headers: {}
|
180 | });
|
181 |
|
182 | req.url = req.url || `${baseUrl}${req.path}`;
|
183 |
|
184 | const queryStr = _(req.query)
|
185 | .map((value, key) => ([key, value]))
|
186 | .filter(([key, value]) => !_.isUndefined(value))
|
187 | .map((keyVal) => keyVal.join('='))
|
188 | .join('&');
|
189 |
|
190 | if (queryStr.length > 0) {
|
191 | req.url += `?${queryStr}`;
|
192 | }
|
193 | req.headers = _.defaults(req.headers, defaultHeaders);
|
194 |
|
195 | req.body = req.body && JSON.stringify(req.body);
|
196 |
|
197 | return fetch(req.url, req)
|
198 | .catch((err) => {
|
199 | debug(`Network error while executing ${req.method} ${req.path}`, err);
|
200 | return Promise.reject(new CraftAiNetworkError({
|
201 | more: err.message
|
202 | }));
|
203 | })
|
204 | .then((res) => res.text()
|
205 | .catch((err) => {
|
206 | debug(`Invalid response from ${req.method} ${req.path}`, err);
|
207 |
|
208 | throw new CraftAiInternalError('Internal Error, the craft ai server responded an invalid response, see err.more for details.', {
|
209 | request: req,
|
210 | more: err.message
|
211 | });
|
212 | })
|
213 | .then((resBody) => parseResponse(req, res, resBody))
|
214 | );
|
215 | };
|
216 | }
|