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

nestjs / nest / e1a120dc-6519-4c6a-a074-de38a9ffd346

14 Feb 2025 09:25PM UTC coverage: 27.513% (-61.8%) from 89.294%
e1a120dc-6519-4c6a-a074-de38a9ffd346

Pull #14640

circleci

luddwichr
fix(platform-express) respect existing parser middlewares when using Express 5

Express 5 made the router public API again and renamed the field from app._router to app.router.
This broke the detection mechanism whether a middleware named "jsonParser" or "urlencodedParser"
is already registered or not.
Unfortunately, https://github.com/nestjs/nest/pull/14574/ only fixed the issue partially.
This commit now uses app.router everywhere.
To avoid future regressions a test was added to verify the expected behavior.
Pull Request #14640: fix(platform-express) respect custom parser middlewares in Express 5

250 of 3354 branches covered (7.45%)

2201 of 8000 relevant lines covered (27.51%)

0.57 hits per line

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

13.64
/packages/common/pipes/parse-array.pipe.ts
1
import { Injectable } from '../decorators/core/injectable.decorator';
1✔
2
import { Optional } from '../decorators/core/optional.decorator';
1✔
3
import { HttpStatus } from '../enums/http-status.enum';
1✔
4
import { Type } from '../interfaces';
5
import {
6
  ArgumentMetadata,
7
  PipeTransform,
8
} from '../interfaces/features/pipe-transform.interface';
9
import { HttpErrorByCode } from '../utils/http-error-by-code.util';
1✔
10
import { isNil, isString, isUndefined } from '../utils/shared.utils';
1✔
11
import { ValidationPipe, ValidationPipeOptions } from './validation.pipe';
1✔
12

13
const VALIDATION_ERROR_MESSAGE = 'Validation failed (parsable array expected)';
1✔
14
const DEFAULT_ARRAY_SEPARATOR = ',';
1✔
15

16
/**
17
 * @publicApi
18
 */
19
export interface ParseArrayOptions
20
  extends Omit<
21
    ValidationPipeOptions,
22
    'transform' | 'validateCustomDecorators' | 'exceptionFactory'
23
  > {
24
  /**
25
   * Type for items to be converted into
26
   */
27
  items?: Type<unknown>;
28
  /**
29
   * Items separator to split string by
30
   * @default ','
31
   */
32
  separator?: string;
33
  /**
34
   * If true, the pipe will return null or undefined if the value is not provided
35
   * @default false
36
   */
37
  optional?: boolean;
38
  /**
39
   * A factory function that returns an exception object to be thrown
40
   * if validation fails.
41
   * @param error Error message or object
42
   * @returns The exception object
43
   */
44
  exceptionFactory?: (error: any) => any;
45
}
46

47
/**
48
 * Defines the built-in ParseArray Pipe
49
 *
50
 * @see [Built-in Pipes](https://docs.nestjs.com/pipes#built-in-pipes)
51
 *
52
 * @publicApi
53
 */
54
@Injectable()
55
export class ParseArrayPipe implements PipeTransform {
1✔
56
  protected readonly validationPipe: ValidationPipe;
57
  protected exceptionFactory: (error: string) => any;
58

59
  constructor(@Optional() protected readonly options: ParseArrayOptions = {}) {
×
60
    this.validationPipe = new ValidationPipe({
×
61
      transform: true,
62
      validateCustomDecorators: true,
63
      ...options,
64
    });
65

66
    const { exceptionFactory, errorHttpStatusCode = HttpStatus.BAD_REQUEST } =
×
67
      options;
×
68
    this.exceptionFactory =
×
69
      exceptionFactory ||
×
70
      (error => new HttpErrorByCode[errorHttpStatusCode](error));
×
71
  }
72

73
  /**
74
   * Method that accesses and performs optional transformation on argument for
75
   * in-flight requests.
76
   *
77
   * @param value currently processed route argument
78
   * @param metadata contains metadata about the currently processed route argument
79
   */
80
  async transform(value: any, metadata: ArgumentMetadata): Promise<any> {
81
    if (!value && !this.options.optional) {
×
82
      throw this.exceptionFactory(VALIDATION_ERROR_MESSAGE);
×
83
    } else if (isNil(value) && this.options.optional) {
×
84
      return value;
×
85
    }
86

87
    if (!Array.isArray(value)) {
×
88
      if (!isString(value)) {
×
89
        throw this.exceptionFactory(VALIDATION_ERROR_MESSAGE);
×
90
      } else {
91
        try {
×
92
          value = value
×
93
            .trim()
94
            .split(this.options.separator || DEFAULT_ARRAY_SEPARATOR);
×
95
        } catch {
96
          throw this.exceptionFactory(VALIDATION_ERROR_MESSAGE);
×
97
        }
98
      }
99
    }
100
    if (this.options.items) {
×
101
      const validationMetadata: ArgumentMetadata = {
×
102
        metatype: this.options.items,
103
        type: 'query',
104
      };
105

106
      const isExpectedTypePrimitive = this.isExpectedTypePrimitive();
×
107
      const toClassInstance = (item: any, index?: number) => {
×
108
        if (this.options.items !== String) {
×
109
          try {
×
110
            item = JSON.parse(item);
×
111
          } catch {
112
            // Do nothing
113
          }
114
        }
115
        if (isExpectedTypePrimitive) {
×
116
          return this.validatePrimitive(item, index);
×
117
        }
118
        return this.validationPipe.transform(item, validationMetadata);
×
119
      };
120
      if (this.options.stopAtFirstError === false) {
×
121
        // strict compare to "false" to make sure
122
        // that this option is disabled by default
123
        let errors: string[] = [];
×
124

125
        const targetArray = value as Array<unknown>;
×
126
        for (let i = 0; i < targetArray.length; i++) {
×
127
          try {
×
128
            targetArray[i] = await toClassInstance(targetArray[i]);
×
129
          } catch (err) {
130
            let message: string[] | string;
131
            if (err.getResponse) {
×
132
              const response = err.getResponse();
×
133
              if (Array.isArray(response.message)) {
×
134
                message = response.message.map(
×
135
                  (item: string) => `[${i}] ${item}`,
×
136
                );
137
              } else {
138
                message = `[${i}] ${response.message}`;
×
139
              }
140
            } else {
141
              message = err;
×
142
            }
143
            errors = errors.concat(message);
×
144
          }
145
        }
146
        if (errors.length > 0) {
×
147
          throw this.exceptionFactory(errors as any);
×
148
        }
149
        return targetArray;
×
150
      } else {
151
        value = await Promise.all(value.map(toClassInstance));
×
152
      }
153
    }
154
    return value;
×
155
  }
156

157
  protected isExpectedTypePrimitive(): boolean {
158
    return [Boolean, Number, String].includes(this.options.items as any);
×
159
  }
160

161
  protected validatePrimitive(originalValue: any, index?: number) {
162
    if (this.options.items === Number) {
×
163
      const value =
164
        originalValue !== null && originalValue !== '' ? +originalValue : NaN;
×
165
      if (isNaN(value)) {
×
166
        throw this.exceptionFactory(
×
167
          `${isUndefined(index) ? '' : `[${index}] `}item must be a number`,
×
168
        );
169
      }
170
      return value;
×
171
    } else if (this.options.items === String) {
×
172
      if (!isString(originalValue)) {
×
173
        return `${originalValue}`;
×
174
      }
175
    } else if (this.options.items === Boolean) {
×
176
      if (typeof originalValue !== 'boolean') {
×
177
        throw this.exceptionFactory(
×
178
          `${
179
            isUndefined(index) ? '' : `[${index}] `
×
180
          }item must be a boolean value`,
181
        );
182
      }
183
    }
184
    return originalValue;
×
185
  }
186
}
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

© 2026 Coveralls, Inc