• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ringcentral / ringcentral-js / 5286631680

pending completion
5286631680

push

github

web-flow
chore: fix sandbox status 502 issue (#219)

260 of 335 branches covered (77.61%)

Branch coverage included in aggregate %.

766 of 792 relevant lines covered (96.72%)

41.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.05
/sdk/src/http/Client.ts
1
import {EventEmitter} from 'events';
1✔
2
import * as qs from 'querystring';
1✔
3
import isPlainObject from 'is-plain-object';
1✔
4
import Externals from '../core/Externals';
5

6
function findHeaderName(name, headers) {
7
    name = name.toLowerCase();
212✔
8
    return Object.keys(headers).reduce((res, key) => {
212✔
9
        if (res) return res;
215✔
10
        if (name === key.toLowerCase()) return key;
113✔
11
        return res;
1✔
12
    }, null);
13
}
14

15
export interface ApiError extends Error {
16
    originalMessage?: string;
17
    response?: Response;
18
    request?: Request;
19
}
20

21
export interface ClientOptions {
22
    externals: Externals;
23
    defaultRequestInit: CreateRequestOptions;
24
}
25

26
export enum events {
1✔
27
    beforeRequest = 'beforeRequest',
1✔
28
    requestSuccess = 'requestSuccess',
1✔
29
    requestError = 'requestError',
1✔
30
}
31

32
export default class Client extends EventEmitter {
1✔
33
    public static _contentType = 'Content-Type';
1✔
34

35
    public static _jsonContentType = 'application/json';
1✔
36

37
    public static _multipartContentType = 'multipart/mixed';
1✔
38

39
    public static _urlencodedContentType = 'application/x-www-form-urlencoded';
1✔
40

41
    public static _headerSeparator = ':';
1✔
42

43
    public static _bodySeparator = '\n\n';
1✔
44

45
    public static _boundarySeparator = '--';
1✔
46

47
    public static _unauthorizedStatus = 401;
1✔
48

49
    public static _rateLimitStatus = 429;
1✔
50

51
    public static _allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'];
1✔
52

53
    public static _defaultRequestInit: CreateRequestOptions = {
1✔
54
        credentials: 'include',
55
        mode: 'cors',
56
    };
57

58
    public events = events;
96✔
59

60
    private _externals: Externals;
61

62
    private _defaultRequestInit: CreateRequestOptions = {};
96✔
63

64
    public constructor({externals, defaultRequestInit = {}}: ClientOptions) {
96!
65
        super();
96✔
66
        this._defaultRequestInit = defaultRequestInit;
96✔
67
        this._externals = externals;
96✔
68
    }
69

70
    public async sendRequest(request: Request): Promise<Response> {
1✔
71
        let response;
72
        try {
73
            //TODO Stop request if listeners return false
74
            this.emit(this.events.beforeRequest, request);
209✔
75

76
            response = await this._loadResponse(request);
209✔
77

78
            if (!response.ok) throw new Error('Response has unsuccessful status');
209✔
79

80
            this.emit(this.events.requestSuccess, response, request);
181✔
81

82
            return response;
181✔
83
        } catch (e) {
84
            const error = !e.response ? await this.makeError(e, response, request) : e;
56✔
85

86
            this.emit(this.events.requestError, error);
28✔
87

88
            throw error;
28✔
89
        }
90
    }
91

92
    public async _loadResponse(request: Request): Promise<Response> {
1✔
93
        return this._externals.fetch.call(null, request); // fixed illegal invocation in Chrome
209✔
94
    }
95

96
    /**
97
     * Wraps the JS Error object with transaction information
98
     */
99
    public async makeError(e: any, response: Response = null, request: Request = null): Promise<ApiError> {
30!
100
        // Wrap only if regular error
101
        if (!e.response && !e.originalMessage) {
✔
102
            e.response = response;
29✔
103
            e.request = request;
29✔
104
            e.originalMessage = e.message;
29✔
105
            e.message = (response && (await this.error(response, true))) || e.originalMessage;
29!
106
        }
107

108
        return e;
29✔
109
    }
110

111
    public createRequest(init: CreateRequestOptions = Client._defaultRequestInit): Request {
213!
112
        init = {...this._defaultRequestInit, ...init};
213✔
113
        init.headers = init.headers || {};
213✔
114

115
        // Sanity checks
116
        if (!init.url) throw new Error('Url is not defined');
213!
117
        if (!init.method) init.method = 'GET';
213✔
118
        init.method = init.method.toUpperCase();
213✔
119
        if (init.method && Client._allowedMethods.indexOf(init.method) < 0) {
426✔
120
            throw new Error(`Method has wrong value: ${init.method}`);
1✔
121
        }
122

123
        // Defaults
124
        init.credentials = init.credentials || 'include';
212✔
125
        init.mode = init.mode || 'cors';
212✔
126

127
        // Append Query String
128
        if (init.query) {
129
            init.url = init.url + (init.url.includes('?') ? '&' : '?') + qs.stringify(init.query);
27!
130
        }
131

132
        // Serialize body
133
        if (isPlainObject(init.body) || !init.body) {
311✔
134
            let contentTypeHeaderName = findHeaderName(Client._contentType, init.headers);
212✔
135

136
            if (!contentTypeHeaderName) {
137
                contentTypeHeaderName = Client._contentType;
100✔
138
                init.headers[contentTypeHeaderName] = Client._jsonContentType;
100✔
139
            }
140

141
            const contentType = init.headers[contentTypeHeaderName];
212✔
142

143
            // Assign a new encoded body
144
            if (contentType.includes(Client._jsonContentType)) {
145
                if ((init.method === 'GET' || init.method === 'HEAD') && !!init.body) {
200✔
146
                    // oddly setting body to null still result in TypeError in phantomjs
147
                    init.body = undefined;
×
148
                } else {
149
                    init.body = JSON.stringify(init.body);
100✔
150
                }
151
            } else if (contentType.includes(Client._urlencodedContentType)) {
152
                init.body = qs.stringify(init.body);
112✔
153
            }
154
        }
155

156
        // Create a request with encoded body
157
        const req = new this._externals.Request(init.url, init);
212✔
158

159
        // Keep the original body accessible directly (for mocks)
160
        req.originalBody = init.body;
212✔
161

162
        return req;
212✔
163
    }
164

165
    public _isContentType(contentType, response) {
1✔
166
        return this.getContentType(response).includes(contentType);
9✔
167
    }
168

169
    public getContentType(response) {
1✔
170
        return response.headers.get(Client._contentType) || '';
13!
171
    }
172

173
    public isMultipart(response) {
1✔
174
        return this._isContentType(Client._multipartContentType, response);
8✔
175
    }
176

177
    public isJson(response) {
1✔
178
        return this._isContentType(Client._jsonContentType, response);
1✔
179
    }
180

181
    public async toMultipart(response: Response): Promise<Response[]> {
1✔
182
        return this.isMultipart(response) ? this.multipart(response) : [response];
1!
183
    }
184

185
    public async multipart(response: Response): Promise<Response[]> {
1✔
186
        if (!this.isMultipart(response)) throw new Error('Response is not multipart');
6✔
187

188
        // Step 1. Split multipart response
189

190
        const text = await response.text();
5✔
191

192
        if (!text) throw new Error('No response body');
5✔
193

194
        let boundary;
195

196
        try {
197
            boundary = this.getContentType(response).match(/boundary=([^;]+)/i)[1]; //eslint-disable-line
4✔
198
        } catch (e) {
199
            throw new Error('Cannot find boundary');
1✔
200
        }
201

202
        if (!boundary) throw new Error('Cannot find boundary');
3!
203

204
        const parts = text.toString().split(Client._boundarySeparator + boundary);
3✔
205

206
        if (parts[0].trim() === '') parts.shift();
3!
207
        if (parts[parts.length - 1].trim() === Client._boundarySeparator) parts.pop();
3!
208

209
        if (parts.length < 1) throw new Error('No parts in body');
3!
210

211
        // Step 2. Parse status info
212

213
        const statusInfo = await this._create(parts.shift(), response.status, response.statusText).json();
3✔
214

215
        // Step 3. Parse all other parts
216

217
        return parts.map((part, i) => this._create(part, statusInfo.response[i].status));
5✔
218
    }
219

220
    /**
221
     * Method is used to create Response object from string parts of multipart/mixed response
222
     */
223
    private _create(text = '', status = 200, statusText = 'OK'): Response {
13!
224
        text = text.replace(/\r/g, '');
8✔
225

226
        const headers = new this._externals.Headers();
8✔
227
        const headersAndBody = text.split(Client._bodySeparator);
8✔
228
        const headersText = headersAndBody.length > 1 ? headersAndBody.shift() : '';
8!
229

230
        text = headersAndBody.length > 0 ? headersAndBody.join(Client._bodySeparator) : null;
8!
231

232
        (headersText || '').split('\n').forEach(header => {
8!
233
            const split = header.trim().split(Client._headerSeparator);
16✔
234
            const key = split.shift().trim();
16✔
235
            const value = split.join(Client._headerSeparator).trim();
16✔
236

237
            if (key) headers.append(key, value);
16✔
238
        });
239

240
        return new this._externals.Response(text, {
8✔
241
            headers,
242
            status,
243
            statusText,
244
        });
245
    }
246

247
    public async error(response: Response, skipOKCheck = false): Promise<string> {
31✔
248
        if (response.ok && !skipOKCheck) return null;
30!
249

250
        let msg = (response.status ? `${response.status} ` : '') + (response.statusText ? response.statusText : '');
30!
251

252
        try {
253
            const {message, error_description, description} = await response.clone().json();
30✔
254

255
            if (message) msg = message;
24✔
256
            if (error_description) msg = error_description;
24✔
257
            if (description) msg = description;
24✔
258
        } catch (e) {} //eslint-disable-line
259

260
        return msg;
30✔
261
    }
262

263
    public on(event: events.beforeRequest, listener: (request: Request) => void);
264
    public on(event: events.requestSuccess, listener: (response: Response, request: Request) => void);
265
    public on(event: events.requestError, listener: (error: ApiError) => void);
266
    public on(event: string, listener: (...args) => void) {
1✔
267
        return super.on(event, listener);
40✔
268
    }
269
}
1✔
270

271
export interface CreateRequestOptions extends RequestInit {
272
    url?: string;
273
    body?: any;
274
    query?: any;
275
    headers?: any;
276
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc