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

panates / opra / 25789779296

13 May 2026 09:13AM UTC coverage: 83.027% (-0.008%) from 83.035%
25789779296

push

github

erayhanoglu
1.28.2

3728 of 4806 branches covered (77.57%)

Branch coverage included in aggregate %.

33674 of 40242 relevant lines covered (83.68%)

222.64 hits per line

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

86.18
/packages/http/src/http-handler.ts
1
import * as process from 'node:process';
1✔
2
import typeIs from '@browsery/type-is';
1✔
3
import {
1✔
4
  ArrayType,
1✔
5
  BadRequestError,
1✔
6
  HttpHeaderCodes,
1✔
7
  HttpMediaType,
1✔
8
  HttpOperationResponse,
1✔
9
  HttpParameter,
1✔
10
  HttpStatusCode,
1✔
11
  InternalServerError,
1✔
12
  isBlob,
1✔
13
  isReadableStream,
1✔
14
  IssueSeverity,
1✔
15
  MethodNotAllowedError,
1✔
16
  MimeTypes,
1✔
17
  OperationResult,
1✔
18
  OpraException,
1✔
19
  OpraHttpError,
1✔
20
  OpraSchema,
1✔
21
  safeJsonStringify,
1✔
22
} from '@opra/common';
1✔
23
import { AssetCache, kAssetCache } from '@opra/core';
1✔
24
import { parse as parseContentType } from 'content-type';
1✔
25
import { splitString } from 'fast-tokenizer';
1✔
26
import { md5 } from 'super-fast-md5';
1✔
27
import { asMutable } from 'ts-gems';
1✔
28
import {
1✔
29
  type ErrorIssue,
1✔
30
  toArray,
1✔
31
  ValidationError,
1✔
32
  type Validator,
1✔
33
  vg,
1✔
34
} from 'valgen';
1✔
35
import type { HttpAdapter } from './http-adapter.js';
1✔
36
import { HttpContext } from './http-context.js';
1✔
37
import { wrapException } from './utils/wrap-exception.js';
1✔
38

1✔
39
/**
1✔
40
 * @namespace
1✔
41
 */
1✔
42
export namespace HttpHandler {
1✔
43
  /**
1✔
44
   * @interface ResponseArgs
1✔
45
   */
1✔
46
  export interface ResponseArgs {
1✔
47
    statusCode: number;
1✔
48
    contentType?: string;
1✔
49
    operationResponse?: HttpOperationResponse;
1✔
50
    body?: any;
1✔
51
    projection?: string[] | '*';
1✔
52
  }
1✔
53
}
1✔
54

1✔
55
/**
1✔
56
 * HttpHandler is responsible for processing incoming HTTP requests.
1✔
57
 * It handles request parsing, interceptor execution, and response generation.
1✔
58
 */
1✔
59
export class HttpHandler {
1✔
60
  protected [kAssetCache]: AssetCache;
1✔
61
  onError?: (
1✔
62
    context: HttpContext,
1✔
63
    error: OpraException,
1✔
64
  ) => void | Promise<void>;
1✔
65

1✔
66
  constructor(readonly adapter: HttpAdapter) {
1✔
67
    this[kAssetCache] = adapter[kAssetCache];
23✔
68
  }
23✔
69

1✔
70
  /**
1✔
71
   * Main HTTP request handler.
1✔
72
   *
1✔
73
   * @param context - The HTTP execution context.
1✔
74
   * @returns A promise that resolves when the request is handled.
1✔
75
   * @protected
1✔
76
   */
1✔
77
  async handleRequest(context: HttpContext): Promise<void> {
1✔
78
    const { response } = context;
175✔
79
    try {
175✔
80
      response.setHeader(
175✔
81
        HttpHeaderCodes.X_Opra_Version,
175✔
82
        OpraSchema.SpecVersion,
175✔
83
      );
175✔
84
      // Expose headers if cors enabled
175✔
85
      if (response.getHeader(HttpHeaderCodes.Access_Control_Allow_Origin)) {
175!
86
        // Expose X-Opra-* headers
×
87
        response.appendHeader(
×
88
          HttpHeaderCodes.Access_Control_Expose_Headers,
×
89
          Object.values(HttpHeaderCodes).filter(k =>
×
90
            k.toLowerCase().startsWith('x-opra-'),
×
91
          ),
×
92
        );
×
93
      }
×
94

175✔
95
      // Parse request
175✔
96
      try {
175✔
97
        await this.parseRequest(context);
175✔
98
      } catch (e: any) {
175!
99
        if (e instanceof OpraException) throw e;
×
100
        if (e instanceof ValidationError) {
×
101
          throw new BadRequestError(
×
102
            {
×
103
              message: 'Request validation failed',
×
104
              code: 'REQUEST_VALIDATION',
×
105
              details: e.issues,
×
106
            },
×
107
            e,
×
108
          );
×
109
        }
×
110
        throw new BadRequestError(e);
×
111
      }
×
112

175✔
113
      await this.adapter.emitAsync('request', context);
175✔
114

175✔
115
      // Call interceptors than execute request
175✔
116
      if (this.adapter.interceptors) {
175✔
117
        const interceptors = this.adapter.interceptors;
175✔
118
        let i = 0;
175✔
119
        const next = async () => {
175✔
120
          const interceptor = interceptors[i++];
177✔
121
          if (typeof interceptor === 'function')
177✔
122
            await interceptor(context, next);
177✔
123
          else if (typeof interceptor?.intercept === 'function')
175!
124
            await interceptor.intercept(context, next);
175!
125
          await this._executeRequest(context);
177✔
126
        };
177✔
127
        await next();
175✔
128
      } else await this._executeRequest(context);
175!
129
    } catch (error: any) {
175!
130
      let e = error;
×
131
      if (e instanceof ValidationError) {
×
132
        e = new InternalServerError(
×
133
          {
×
134
            message: 'Response validation failed',
×
135
            code: 'RESPONSE_VALIDATION',
×
136
            details: e.issues,
×
137
          },
×
138
          e,
×
139
        );
×
140
      } else e = wrapException(e);
×
141
      if (this.onError) await this.onError(context, error);
×
142
      context.errors.push(e);
×
143
      await this.sendResponse(context);
×
144
    } finally {
175✔
145
      await context.emitAsync('finish', context);
175✔
146
    }
175✔
147
  }
175✔
148

1✔
149
  /**
1✔
150
   * Parses the HTTP request, including parameters and content type.
1✔
151
   *
1✔
152
   * @param context - The HTTP execution context.
1✔
153
   * @returns A promise that resolves when the request is parsed.
1✔
154
   */
1✔
155
  async parseRequest(context: HttpContext): Promise<void> {
1✔
156
    await this._parseParameters(context);
187✔
157
    await this._parseContentType(context);
185✔
158
    if (context.__oprDef?.requestBody?.immediateFetch) await context.getBody();
187✔
159
    /* Set default status code as the first status code between 200 and 299 */
184✔
160
    if (context.__oprDef) {
184✔
161
      for (const r of context.__oprDef.responses) {
184✔
162
        const st = r.statusCode.find(sc => sc.start <= 299 && sc.end >= 200);
171✔
163
        if (st) {
171✔
164
          context.response.status(st.start);
171✔
165
          break;
171✔
166
        }
171✔
167
      }
171✔
168
    }
184✔
169
  }
187✔
170

1✔
171
  /**
1✔
172
   * Parses various HTTP parameters (cookies, headers, path, query).
1✔
173
   *
1✔
174
   * @param context - The HTTP execution context.
1✔
175
   * @returns A promise that resolves when parameters are parsed.
1✔
176
   * @throws {@link BadRequestError} If parameter validation fails.
1✔
177
   * @protected
1✔
178
   */
1✔
179
  protected async _parseParameters(context: HttpContext) {
1✔
180
    const { __oprDef, request } = context;
187✔
181
    if (!__oprDef) return;
187!
182

187✔
183
    let key: string = '';
187✔
184
    try {
187✔
185
      const onFail = (issue: ErrorIssue) => {
187✔
186
        issue.location = key;
2✔
187
        return issue;
2✔
188
      };
2✔
189
      /* prepare decoders */
187✔
190

187✔
191
      const getDecoder = (prm: HttpParameter): Validator => {
187✔
192
        let decode = this[kAssetCache].get<Validator>(prm, 'decode');
153✔
193
        if (!decode) {
153✔
194
          decode = prm.generateCodec('decode', {
46✔
195
            scope: this.adapter.scope,
46✔
196
            ignoreReadonlyFields: true,
46✔
197
          });
46✔
198
          this[kAssetCache].set(prm, 'decode', decode);
46✔
199
        }
46✔
200
        return decode;
153✔
201
      };
153✔
202

187✔
203
      const paramsLeft = new Set([
187✔
204
        ...__oprDef.parameters,
187✔
205
        ...__oprDef.owner.parameters,
187✔
206
      ]);
187✔
207

187✔
208
      /* parse cookie parameters */
187✔
209
      if (request.cookies) {
187✔
210
        for (key of Object.keys(request.cookies)) {
2✔
211
          const oprPrm = __oprDef.findParameter(key, 'cookie');
3✔
212
          const cntPrm = __oprDef.owner.findParameter(key, 'cookie');
3✔
213
          const prm = oprPrm || cntPrm;
3✔
214
          if (!prm) continue;
3✔
215
          if (oprPrm) paramsLeft.delete(oprPrm);
3✔
216
          if (cntPrm) paramsLeft.delete(cntPrm);
3✔
217
          const decode = getDecoder(prm);
2✔
218
          const v: any = decode(request.cookies[key], {
2✔
219
            coerce: true,
2✔
220
            label: key,
2✔
221
            onFail,
2✔
222
          });
2✔
223
          const prmName = typeof prm.name === 'string' ? prm.name : key;
3!
224
          if (v !== undefined) context.cookies[prmName] = v;
3✔
225
        }
3✔
226
      }
2✔
227

187✔
228
      /* parse headers */
187✔
229
      if (request.headers) {
187✔
230
        for (key of Object.keys(request.headers)) {
187✔
231
          const oprPrm = __oprDef.findParameter(key, 'header');
1,262✔
232
          const cntPrm = __oprDef.owner.findParameter(key, 'header');
1,262✔
233
          const prm = oprPrm || cntPrm;
1,262✔
234
          if (!prm) continue;
1,262✔
235
          if (oprPrm) paramsLeft.delete(oprPrm);
1,262✔
236
          if (cntPrm) paramsLeft.delete(cntPrm);
1,262✔
237
          const decode = getDecoder(prm);
2✔
238
          const v: any = decode(request.headers[key], {
2✔
239
            coerce: true,
2✔
240
            label: key,
2✔
241
            onFail,
2✔
242
          });
2✔
243
          const prmName = typeof prm.name === 'string' ? prm.name : key;
1,262!
244
          if (v !== undefined) context.headers[prmName] = v;
1,262✔
245
        }
1,262✔
246
      }
187✔
247

187✔
248
      /* parse path parameters */
187✔
249
      if (request.params) {
187✔
250
        for (key of Object.keys(request.params)) {
187✔
251
          const oprPrm = __oprDef.findParameter(key, 'path');
45✔
252
          const cntPrm = __oprDef.owner.findParameter(key, 'path');
45✔
253
          const prm = oprPrm || cntPrm;
45✔
254
          if (!prm) continue;
45!
255
          if (oprPrm) paramsLeft.delete(oprPrm);
45!
256
          if (cntPrm) paramsLeft.delete(cntPrm);
45✔
257
          const decode = getDecoder(prm);
45✔
258
          const v: any = decode(request.params[key], {
45✔
259
            coerce: true,
45✔
260
            label: key,
45✔
261
            onFail,
45✔
262
          });
45✔
263
          if (v !== undefined) context.pathParams[key] = v;
45✔
264
        }
45✔
265
      }
187✔
266

187✔
267
      /* parse query parameters */
187✔
268
      const url = new URL(
187✔
269
        request.originalUrl || request.url || '/',
187✔
270
        'http://tempuri.org',
187✔
271
      );
187✔
272
      const { searchParams } = url;
187✔
273
      for (key of searchParams.keys()) {
187✔
274
        const oprPrm = __oprDef.findParameter(key, 'query');
92✔
275
        const cntPrm = __oprDef.owner.findParameter(key, 'query');
92✔
276
        const prm = oprPrm || cntPrm;
92✔
277
        if (!prm) continue;
92✔
278
        if (oprPrm) paramsLeft.delete(oprPrm);
91✔
279
        if (cntPrm) paramsLeft.delete(cntPrm);
92!
280
        const decode = getDecoder(prm);
91✔
281
        let values: any[] = searchParams?.getAll(key);
92✔
282
        const prmName = typeof prm.name === 'string' ? prm.name : key;
92!
283
        if (values?.length && (prm.type instanceof ArrayType || prm.isArray)) {
92✔
284
          values = values
52✔
285
            .map(v =>
52✔
286
              splitString(v, {
52✔
287
                delimiters: prm.arraySeparator || ',',
52!
288
                quotes: true,
52✔
289
              }),
52✔
290
            )
52✔
291
            .flat();
52✔
292
          if (prm.type instanceof ArrayType)
52✔
293
            values = decode(values, { coerce: true, label: key, onFail });
52!
294
          else
52✔
295
            values = values.map(v =>
52✔
296
              decode(v, { coerce: true, label: key, onFail }),
82✔
297
            );
52✔
298
          if (prm.parser) values = prm.parser(values);
52✔
299
          if (values.length) context.queryParams[prmName] = values;
52✔
300
        } else {
92✔
301
          let v = decode(values[0], { coerce: true, label: key, onFail });
39✔
302
          if (prm.parser) v = prm.parser(v);
39!
303
          if (values.length) context.queryParams[prmName] = v;
38✔
304
        }
39✔
305
      }
92✔
306

186✔
307
      for (const prm of paramsLeft) {
187✔
308
        key = String(prm.name);
333✔
309
        // Throw error for required parameters
333✔
310
        if (prm.default && typeof prm.name === 'string') {
333✔
311
          context.queryParams[prm.name] = prm.default;
1✔
312
        } else if (prm.required) {
333✔
313
          const decode = getDecoder(prm);
13✔
314
          decode(undefined, { coerce: true, label: String(prm.name), onFail });
13✔
315
        }
13✔
316
      }
333✔
317
    } catch (e: any) {
187✔
318
      if (e instanceof ValidationError) {
2✔
319
        throw new BadRequestError(
2✔
320
          {
2✔
321
            message: `Invalid parameter (${key}) value. ` + e.message,
2✔
322
            code: 'REQUEST_VALIDATION',
2✔
323
            details: e.issues,
2✔
324
          },
2✔
325
          e,
2✔
326
        );
2✔
327
      }
2!
328
      throw e;
×
329
    }
×
330
  }
187✔
331

1✔
332
  /**
1✔
333
   * Parses and validates the request content type.
1✔
334
   *
1✔
335
   * @param context - The HTTP execution context.
1✔
336
   * @returns A promise that resolves when content type is parsed.
1✔
337
   * @throws {@link BadRequestError} If the content type is invalid or missing.
1✔
338
   * @protected
1✔
339
   */
1✔
340
  protected async _parseContentType(context: HttpContext) {
1✔
341
    const { request, __oprDef } = context;
185✔
342
    if (!__oprDef) return;
185!
343
    if (__oprDef.requestBody?.content.length) {
185✔
344
      let mediaType: HttpMediaType | undefined;
61✔
345
      let contentType = request.header('content-type');
61✔
346
      if (contentType) {
61✔
347
        const ct = parseContentType(contentType);
61✔
348
        contentType = ct.type;
61✔
349
        mediaType = __oprDef.requestBody.content.find(
61✔
350
          mc =>
61✔
351
            mc.contentType &&
61✔
352
            typeIs.is(
61✔
353
              contentType!,
61✔
354
              Array.isArray(mc.contentType) ? mc.contentType : [mc.contentType],
61✔
355
            ),
61✔
356
        );
61✔
357
        if (mediaType) mediaType = Object.create(mediaType);
61✔
358
        if (ct && mediaType) {
61✔
359
          mediaType.contentType = contentType;
60✔
360
          mediaType.contentEncoding =
60✔
361
            ct.parameters?.['charset'] || mediaType.contentEncoding;
60✔
362
        }
60✔
363
      }
61✔
364
      if (!mediaType) {
61✔
365
        const contentTypes = __oprDef.requestBody.content
1✔
366
          .map(mc => mc.contentType)
1✔
367
          .flat();
1✔
368
        throw new BadRequestError(
1✔
369
          `Request body should be one of required content types (${contentTypes.join(', ')})`,
1✔
370
        );
1✔
371
      }
1✔
372
      asMutable(context).mediaType = mediaType;
60✔
373
    }
60✔
374
  }
185✔
375

1✔
376
  /**
1✔
377
   *
1✔
378
   * @param context
1✔
379
   * @protected
1✔
380
   */
1✔
381
  protected async _executeRequest(context: HttpContext): Promise<any> {
1✔
382
    if (!context.__handler) throw new MethodNotAllowedError();
177!
383
    const responseValue = await context.__handler.call(
177✔
384
      context.__controller,
177✔
385
      context,
177✔
386
    );
177✔
387
    const { response } = context;
177✔
388
    if (!response.writableEnded) {
177✔
389
      await this.sendResponse(context, responseValue).finally(() => {
175✔
390
        if (!response.writableEnded) response.end();
175!
391
      });
175✔
392
    }
175✔
393
  }
177✔
394

1✔
395
  /**
1✔
396
   * Sends an HTTP response back to the client.
1✔
397
   *
1✔
398
   * @param context - The HTTP execution context.
1✔
399
   * @param responseValue - The value to be sent in the response body.
1✔
400
   * @returns A promise that resolves when the response is sent.
1✔
401
   */
1✔
402
  async sendResponse(context: HttpContext, responseValue?: any): Promise<void> {
1✔
403
    if (context.errors.length) return this._sendErrorResponse(context);
180✔
404
    const { response } = context;
175✔
405
    const { document } = this.adapter;
175✔
406
    try {
175✔
407
      const responseArgs = this._determineResponseArgs(context, responseValue);
175✔
408

175✔
409
      const { operationResponse, statusCode } = responseArgs;
175✔
410
      let { contentType, body } = responseArgs;
175✔
411

175✔
412
      const operationResultType = document.node.getDataType(OperationResult);
175✔
413
      let operationResultEncoder = this[kAssetCache].get<Validator>(
175✔
414
        operationResultType,
175✔
415
        'encode',
175✔
416
      );
175✔
417
      if (!operationResultEncoder) {
180✔
418
        operationResultEncoder = operationResultType.generateCodec('encode', {
8✔
419
          scope: this.adapter.scope,
8✔
420
          ignoreWriteonlyFields: true,
8✔
421
        });
8✔
422
        this[kAssetCache].set(
8✔
423
          operationResultType,
8✔
424
          'encode',
8✔
425
          operationResultEncoder,
8✔
426
        );
8✔
427
      }
8✔
428

175✔
429
      /* Validate response */
175✔
430
      if (operationResponse?.type) {
180✔
431
        if (
119✔
432
          !(
119✔
433
            body == null &&
119!
434
            (statusCode as HttpStatusCode) === HttpStatusCode.NO_CONTENT
×
435
          )
119✔
436
        ) {
119✔
437
          /* Generate encoder */
119✔
438
          const projection = responseArgs.projection || '*';
119✔
439
          const assetKey = md5(String(projection));
119✔
440
          let encode = this[kAssetCache].get<Validator>(
119✔
441
            operationResponse,
119✔
442
            'encode:' + assetKey,
119✔
443
          );
119✔
444
          if (!encode) {
119✔
445
            encode = operationResponse.type.generateCodec('encode', {
64✔
446
              scope: this.adapter.scope,
64✔
447
              partial: operationResponse.partial,
64✔
448
              projection,
64✔
449
              ignoreWriteonlyFields: true,
64✔
450
              onFail: issue =>
64✔
451
                `Response body validation failed: ` + issue.message,
64✔
452
            });
64✔
453
            if (operationResponse) {
64✔
454
              if (operationResponse.isArray) encode = vg.isArray(encode);
64✔
455
              this[kAssetCache].set(
64✔
456
                operationResponse,
64✔
457
                'encode:' + assetKey,
64✔
458
                encode,
64✔
459
              );
64✔
460
            }
64✔
461
          }
64✔
462
          /* Encode body */
119✔
463
          if (operationResponse.type.extendsFrom(operationResultType)) {
119!
464
            if (body instanceof OperationResult) body = encode(body);
×
465
            else {
×
466
              body.payload = encode(body.payload);
×
467
              body = operationResultEncoder(body);
×
468
            }
×
469
          } else {
119✔
470
            if (
119✔
471
              body instanceof OperationResult &&
119✔
472
              contentType &&
114✔
473
              typeIs.is(contentType, [MimeTypes.opra_response_json])
114✔
474
            ) {
119✔
475
              body.payload = encode(body.payload);
114✔
476
              body = operationResultEncoder(body);
114✔
477
            } else {
119✔
478
              body = encode(body);
5✔
479
            }
5✔
480
          }
119✔
481

119✔
482
          if (
119✔
483
            body instanceof OperationResult &&
119✔
484
            operationResponse.type &&
119✔
485
            operationResponse.type !==
114✔
486
              document.node.getDataType(OperationResult)
114✔
487
          ) {
119✔
488
            body.type = operationResponse.type.name
114✔
489
              ? operationResponse.type.name
114✔
490
              : '#embedded';
×
491
          }
114✔
492
        }
119✔
493
      } else if (body != null) {
180✔
494
        if (body instanceof OperationResult) {
43✔
495
          body = operationResultEncoder(body);
31✔
496
          contentType = MimeTypes.opra_response_json;
31✔
497
        } else if (Buffer.isBuffer(body)) contentType = MimeTypes.binary;
43!
498
        else if (typeof body === 'object') {
12✔
499
          contentType = contentType || MimeTypes.json;
12✔
500
          if (typeof body.toJSON === 'function') body = body.toJSON();
12!
501
        } else {
12!
502
          contentType = contentType || MimeTypes.text;
×
503
          body = String(body);
×
504
        }
×
505
      }
43✔
506
      /* Set content-type header value if not set */
175✔
507
      if (contentType && contentType !== responseArgs.contentType)
180✔
508
        response.setHeader('content-type', contentType);
180✔
509

175✔
510
      response.status(statusCode);
175✔
511
      if (body == null) {
180✔
512
        response.end();
13✔
513
        return;
13✔
514
      }
13✔
515

162✔
516
      let x: any;
162✔
517
      if (Buffer.isBuffer(body) || isReadableStream(body)) x = body;
180!
518
      else if (isBlob(body)) x = body.stream();
162!
519
      else if (typeof body === 'object') x = JSON.stringify(body);
162!
520
      else x = String(body);
×
521
      response.end(x);
162✔
522
    } catch (error: any) {
180!
523
      context.errors.push(error);
×
524
      return this._sendErrorResponse(context);
×
525
    }
×
526
  }
180✔
527

1✔
528
  protected async _sendErrorResponse(context: HttpContext): Promise<void> {
1✔
529
    let errors = (context.errors = this._wrapExceptions(context.errors));
5✔
530
    try {
5✔
531
      if (context.listenerCount('error')) {
5✔
532
        await context.emitAsync('error', errors[0], context);
5✔
533
        errors = context.errors = this._wrapExceptions(context.errors);
5✔
534
      }
5✔
535
      if (this.adapter.listenerCount('error')) {
5✔
536
        await this.adapter.emitAsync('error', errors[0], context);
5✔
537
        errors = context.errors = this._wrapExceptions(errors);
5✔
538
      }
5✔
539
      if (this.adapter.logger?.error) {
5✔
540
        const logger = this.adapter.logger;
4✔
541
        errors.forEach(e => {
4✔
542
          if (e.status >= 500 && e.status < 600) logger.error(e);
4!
543
        });
4✔
544
      }
4✔
545
    } catch (e) {
5!
546
      context.errors = this._wrapExceptions([e, ...context.errors]);
×
547
    }
×
548

5✔
549
    const { response } = context;
5✔
550
    if (response.headersSent) {
5!
551
      response.end();
×
552
      return;
×
553
    }
×
554

5✔
555
    let status = response.statusCode || 0;
5!
556
    if (!status || status < Number(HttpStatusCode.BAD_REQUEST)) {
5✔
557
      status = errors[0].status;
5✔
558
      if (status < Number(HttpStatusCode.BAD_REQUEST))
5✔
559
        status = HttpStatusCode.INTERNAL_SERVER_ERROR;
5!
560
    }
5✔
561
    response.statusCode = status;
5✔
562

5✔
563
    const { document } = this.adapter;
5✔
564
    const dt = document.node.getComplexType('OperationResult');
5✔
565
    let encode = this[kAssetCache].get<Validator>(dt, 'encode');
5✔
566
    if (!encode) {
5✔
567
      encode = dt.generateCodec('encode', {
1✔
568
        scope: this.adapter.scope,
1✔
569
        ignoreWriteonlyFields: true,
1✔
570
      });
1✔
571
      this[kAssetCache].set(dt, 'encode', encode);
1✔
572
    }
1✔
573
    // const { i18n } = this.adapter;
5✔
574
    const bodyObject = new OperationResult({
5✔
575
      errors: errors.map(x => {
5✔
576
        const o = x.toJSON();
5✔
577
        if (
5✔
578
          !(
5✔
579
            process.env.NODE_ENV === 'dev' ||
5✔
580
            process.env.NODE_ENV === 'development'
5✔
581
          )
5✔
582
        )
5✔
583
          delete o.stack;
5✔
584
        return o; // i18n.deep(o);
5✔
585
      }),
5✔
586
    });
5✔
587
    const body = encode(bodyObject);
5✔
588

5✔
589
    response.setHeader(
5✔
590
      HttpHeaderCodes.Content_Type,
5✔
591
      MimeTypes.opra_response_json + '; charset=utf-8',
5✔
592
    );
5✔
593
    response.setHeader(HttpHeaderCodes.Cache_Control, 'no-cache');
5✔
594
    response.setHeader(HttpHeaderCodes.Pragma, 'no-cache');
5✔
595
    response.setHeader(HttpHeaderCodes.Expires, '-1');
5✔
596
    response.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
5✔
597
    response.send(safeJsonStringify(body));
5✔
598
    response.end();
5✔
599
  }
5✔
600

1✔
601
  /**
1✔
602
   * Sends the document schema as a JSON response.
1✔
603
   *
1✔
604
   * @param context - The HTTP execution context.
1✔
605
   * @returns A promise that resolves when the schema is sent.
1✔
606
   */
1✔
607
  async sendDocumentSchema(context: HttpContext): Promise<void> {
1✔
608
    const { request, response } = context;
5✔
609
    const { document } = this.adapter;
5✔
610
    response.setHeader('content-type', MimeTypes.json);
5✔
611
    const url = new URL(
5✔
612
      request.originalUrl || request.url || '/',
5!
613
      'http://tempuri.org',
5✔
614
    );
5✔
615
    const { searchParams } = url;
5✔
616
    const documentId = searchParams.get('id');
5✔
617
    const doc = documentId ? document.findDocument(documentId) : document;
5✔
618
    if (!doc) {
5!
619
      context.errors.push(
×
620
        new BadRequestError({
×
621
          message: `Document with given id [${documentId}] does not exists`,
×
622
        }),
×
623
      );
×
624
      return this.sendResponse(context);
×
625
    }
×
626
    /* Check if response cache exists */
5✔
627
    let responseBody = this[kAssetCache].get(doc, `$schema`);
5✔
628
    /* Create response if response cache does not exists */
5✔
629
    if (!responseBody) {
5✔
630
      const schema = doc.export({
5✔
631
        scope: this.adapter.scope,
5✔
632
      });
5✔
633
      responseBody = JSON.stringify(schema);
5✔
634
      this[kAssetCache].set(doc, `$schema`, responseBody);
5✔
635
    }
5✔
636
    response.end(responseBody);
5✔
637
  }
5✔
638

1✔
639
  /**
1✔
640
   * Determines the response arguments (status code, content type, etc.) for a given response value.
1✔
641
   *
1✔
642
   * @param context - The HTTP execution context.
1✔
643
   * @param body - The response body.
1✔
644
   * @returns The determined response arguments.
1✔
645
   * @throws {@link InternalServerError} If response configuration is missing or invalid.
1✔
646
   * @protected
1✔
647
   */
1✔
648
  protected _determineResponseArgs(
1✔
649
    context: HttpContext,
175✔
650
    body: any,
175✔
651
  ): HttpHandler.ResponseArgs {
175✔
652
    const { response, __oprDef } = context;
175✔
653

175✔
654
    const hasBody = body != null;
175✔
655
    const statusCode =
175✔
656
      !hasBody && (response.statusCode as any) === HttpStatusCode.OK
175✔
657
        ? HttpStatusCode.NO_CONTENT
13✔
658
        : response.statusCode;
175✔
659
    /* Parse content-type header */
175✔
660
    const parsedContentType =
175✔
661
      hasBody && response.hasHeader('content-type')
175!
662
        ? parseContentType(response as any)
×
663
        : undefined;
175✔
664
    let contentType = parsedContentType?.type;
175!
665
    /* Estimate content type if not defined */
175✔
666
    if (hasBody && !contentType) {
175✔
667
      if (body instanceof OperationResult)
162✔
668
        contentType = MimeTypes.opra_response_json;
162✔
669
      else if (Buffer.isBuffer(body)) contentType = MimeTypes.binary;
150!
670
    }
162✔
671
    let operationResponse: HttpOperationResponse | undefined;
175✔
672

175✔
673
    const cacheKey = `HttpOperationResponse:${statusCode}${contentType ? ':' + contentType : ''}`;
175✔
674
    let responseArgs = this[kAssetCache].get<HttpHandler.ResponseArgs>(
175✔
675
      response,
175✔
676
      cacheKey,
175✔
677
    );
175✔
678
    if (!responseArgs) {
175✔
679
      responseArgs = { statusCode, contentType } as HttpHandler.ResponseArgs;
175✔
680

175✔
681
      if (__oprDef?.responses.length) {
175✔
682
        /* Filter available HttpOperationResponse instances according to status code. */
163✔
683
        const filteredResponses = __oprDef.responses.filter(r =>
163✔
684
          r.statusCode.find(
387✔
685
            sc => sc.start <= statusCode && sc.end >= statusCode,
387✔
686
          ),
163✔
687
        );
163✔
688

163✔
689
        /* Throw InternalServerError if controller returns non-configured status code */
163✔
690
        if (!filteredResponses.length && statusCode < 400) {
163!
691
          throw new InternalServerError(
×
692
            `No responses defined for status code ${statusCode} in operation "${__oprDef.name}"`,
×
693
          );
×
694
        }
×
695

163✔
696
        /* We search for content-type in filtered HttpOperationResponse array */
163✔
697
        if (filteredResponses.length) {
163✔
698
          /* If no response returned, and content-type has not been set (No response wants to be returned by operation) */
163✔
699
          if (!hasBody) {
163✔
700
            /* Find HttpOperationResponse with no content-type */
13✔
701
            operationResponse = filteredResponses.find(r => !r.contentType);
13✔
702
          }
13✔
703

163✔
704
          if (!operationResponse) {
163✔
705
            /* Find HttpOperationResponse according to content-type */
150✔
706
            if (contentType) {
150✔
707
              // Find HttpEndpointResponse instance according to content-type header
12✔
708
              operationResponse = filteredResponses.find(r =>
12✔
709
                typeIs.is(contentType!, toArray(r.contentType)),
12✔
710
              );
12✔
711
              if (!operationResponse) {
12!
712
                throw new InternalServerError(
×
713
                  `Operation didn't configured to return "${contentType}" content`,
×
714
                );
×
715
              }
×
716
            } else {
150✔
717
              /* Select first HttpOperationResponse if content-type header has not been set */
138✔
718
              operationResponse = filteredResponses[0];
138✔
719
              if (operationResponse.contentType) {
138✔
720
                const ct = typeIs.normalize(
138✔
721
                  Array.isArray(operationResponse.contentType)
138!
722
                    ? operationResponse.contentType[0]
×
723
                    : operationResponse.contentType,
138✔
724
                );
138✔
725
                if (typeof ct === 'string')
138✔
726
                  responseArgs.contentType = contentType = ct;
138!
727
                else if (operationResponse.type)
×
728
                  responseArgs.contentType = MimeTypes.opra_response_json;
×
729
              }
138✔
730
            }
138✔
731
          }
150✔
732
          responseArgs.operationResponse = operationResponse;
163✔
733
          if (
163✔
734
            !operationResponse.statusCode.find(
163✔
735
              sc => sc.start <= statusCode && sc.end >= statusCode,
163✔
736
            )
163✔
737
          ) {
163!
738
            responseArgs.statusCode = operationResponse.statusCode[0].start;
×
739
          }
×
740
        }
163✔
741
      }
163✔
742
      if (!hasBody) delete responseArgs.contentType;
175✔
743
      if (__oprDef?.composition?.startsWith('Entity.')) {
175✔
744
        if (context.queryParams.projection)
158✔
745
          responseArgs.projection = context.queryParams.projection;
158✔
746
      }
158✔
747
      this[kAssetCache].set(response, cacheKey, { ...responseArgs });
175✔
748
    }
175✔
749

175✔
750
    /* Fix response value according to composition */
175✔
751
    const composition = operationResponse?.owner.composition;
175✔
752
    if (composition && body != null) {
175✔
753
      switch (composition) {
145✔
754
        case 'Entity.Create':
145✔
755
        case 'Entity.Get':
145✔
756
        case 'Entity.FindMany':
145✔
757
        case 'Entity.Update': {
145✔
758
          if (!(body instanceof OperationResult)) {
114✔
759
            body = new OperationResult({
102✔
760
              payload: body,
102✔
761
            });
102✔
762
          }
102✔
763
          if (
114✔
764
            (composition === 'Entity.Create' ||
114✔
765
              composition === 'Entity.Update') &&
114✔
766
            composition &&
48✔
767
            body.affected == null
48✔
768
          ) {
114✔
769
            body.affected = 1;
48✔
770
          }
48✔
771
          break;
114✔
772
        }
114✔
773
        case 'Entity.Delete':
145✔
774
        case 'Entity.DeleteMany':
145✔
775
        case 'Entity.UpdateMany': {
145✔
776
          if (!(body instanceof OperationResult)) {
31✔
777
            body = new OperationResult({
31✔
778
              affected: body,
31✔
779
            });
31✔
780
          }
31✔
781
          body.affected =
31✔
782
            typeof body.affected === 'number'
31✔
783
              ? body.affected
31✔
784
              : typeof body.affected === 'boolean'
×
785
                ? body.affected
×
786
                  ? 1
×
787
                  : 0
×
788
                : undefined;
×
789
          break;
31✔
790
        }
31✔
791
        default:
145!
792
          break;
×
793
      }
145✔
794
    }
145✔
795

175✔
796
    if (
175✔
797
      responseArgs.contentType &&
175✔
798
      responseArgs.contentType !== parsedContentType?.type
150!
799
    ) {
175✔
800
      response.setHeader('content-type', responseArgs.contentType);
150✔
801
    }
150✔
802
    if (
175✔
803
      responseArgs.contentType &&
175✔
804
      body != null &&
150✔
805
      !(body instanceof OperationResult) &&
150✔
806
      typeIs.is(responseArgs.contentType!, [MimeTypes.opra_response_json])
5✔
807
    ) {
175!
808
      body = new OperationResult({ payload: body });
×
809
    }
×
810

175✔
811
    if (hasBody) responseArgs.body = body;
175✔
812
    return responseArgs;
175✔
813
  }
175✔
814

1✔
815
  protected _wrapExceptions(exceptions: any[]): OpraHttpError[] {
1✔
816
    const wrappedErrors = exceptions.map(wrapException);
15✔
817
    if (!wrappedErrors.length) wrappedErrors.push(new InternalServerError());
15!
818
    // Sort errors from fatal to info
15✔
819
    wrappedErrors.sort((a, b) => {
15✔
820
      const i =
×
821
        IssueSeverity.Keys.indexOf(a.severity) -
×
822
        IssueSeverity.Keys.indexOf(b.severity);
×
823
      if (i === 0) return b.status - a.status;
×
824
      return i;
×
825
    });
×
826
    return wrappedErrors;
15✔
827
  }
15✔
828
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc