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

expressjs / express / 20273959090

16 Dec 2025 03:48PM UTC coverage: 98.815%. First build
20273959090

Pull #6952

github

web-flow
Merge a744171b0 into 9eb700151
Pull Request #6952: feat: add native .env file loading support

64 of 71 new or added lines in 2 files covered. (90.14%)

834 of 844 relevant lines covered (98.82%)

2141.18 hits per line

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

95.21
/lib/utils.js
1
/*!
2
 * express
3
 * Copyright(c) 2009-2013 TJ Holowaychuk
4
 * Copyright(c) 2014-2015 Douglas Christopher Wilson
5
 * MIT Licensed
6
 */
7

8
'use strict';
9

10
/**
11
 * Module dependencies.
12
 * @api private
13
 */
14

15
var { METHODS } = require('node:http');
8✔
16
var contentType = require('content-type');
8✔
17
var etag = require('etag');
8✔
18
var mime = require('mime-types')
8✔
19
var proxyaddr = require('proxy-addr');
8✔
20
var qs = require('qs');
8✔
21
var querystring = require('node:querystring');
8✔
22
const { Buffer } = require('node:buffer');
8✔
23
const fs = require('node:fs');
8✔
24
const path = require('node:path');
8✔
25

26

27
/**
28
 * A list of lowercased HTTP methods that are supported by Node.js.
29
 * @api private
30
 */
31
exports.methods = METHODS.map((method) => method.toLowerCase());
278✔
32

33
/**
34
 * Return strong ETag for `body`.
35
 *
36
 * @param {String|Buffer} body
37
 * @param {String} [encoding]
38
 * @return {String}
39
 * @api private
40
 */
41

42
exports.etag = createETagGenerator({ weak: false })
8✔
43

44
/**
45
 * Return weak ETag for `body`.
46
 *
47
 * @param {String|Buffer} body
48
 * @param {String} [encoding]
49
 * @return {String}
50
 * @api private
51
 */
52

53
exports.wetag = createETagGenerator({ weak: true })
8✔
54

55
/**
56
 * Normalize the given `type`, for example "html" becomes "text/html".
57
 *
58
 * @param {String} type
59
 * @return {Object}
60
 * @api private
61
 */
62

63
exports.normalizeType = function(type){
8✔
64
  return ~type.indexOf('/')
784✔
65
    ? acceptParams(type)
66
    : { value: (mime.lookup(type) || 'application/octet-stream'), params: {} }
67
};
68

69
/**
70
 * Normalize `types`, for example "html" becomes "text/html".
71
 *
72
 * @param {Array} types
73
 * @return {Array}
74
 * @api private
75
 */
76

77
exports.normalizeTypes = function(types) {
8✔
78
  return types.map(exports.normalizeType);
40✔
79
};
80

81

82
/**
83
 * Parse accept params `str` returning an
84
 * object with `.value`, `.quality` and `.params`.
85
 *
86
 * @param {String} str
87
 * @return {Object}
88
 * @api private
89
 */
90

91
function acceptParams (str) {
92
  var length = str.length;
184✔
93
  var colonIndex = str.indexOf(';');
184✔
94
  var index = colonIndex === -1 ? length : colonIndex;
184✔
95
  var ret = { value: str.slice(0, index).trim(), quality: 1, params: {} };
184✔
96

97
  while (index < length) {
184✔
98
    var splitIndex = str.indexOf('=', index);
200✔
99
    if (splitIndex === -1) break;
200✔
100

101
    var colonIndex = str.indexOf(';', index);
192✔
102
    var endIndex = colonIndex === -1 ? length : colonIndex;
192✔
103

104
    if (splitIndex > endIndex) {
192✔
105
      index = str.lastIndexOf(';', splitIndex - 1) + 1;
88✔
106
      continue;
88✔
107
    }
108

109
    var key = str.slice(index, splitIndex).trim();
104✔
110
    var value = str.slice(splitIndex + 1, endIndex).trim();
104✔
111

112
    if (key === 'q') {
104✔
113
      ret.quality = parseFloat(value);
32✔
114
    } else {
115
      ret.params[key] = value;
72✔
116
    }
117

118
    index = endIndex + 1;
104✔
119
  }
120

121
  return ret;
184✔
122
}
123

124
/**
125
 * Compile "etag" value to function.
126
 *
127
 * @param  {Boolean|String|Function} val
128
 * @return {Function}
129
 * @api private
130
 */
131

132
exports.compileETag = function(val) {
8✔
133
  var fn;
134

135
  if (typeof val === 'function') {
7,678✔
136
    return val;
24✔
137
  }
138

139
  switch (val) {
7,654✔
140
    case true:
141
    case 'weak':
142
      fn = exports.wetag;
7,614✔
143
      break;
7,614✔
144
    case false:
145
      break;
24✔
146
    case 'strong':
147
      fn = exports.etag;
8✔
148
      break;
8✔
149
    default:
150
      throw new TypeError('unknown value for etag function: ' + val);
8✔
151
  }
152

153
  return fn;
7,646✔
154
}
155

156
/**
157
 * Compile "query parser" value to function.
158
 *
159
 * @param  {String|Function} val
160
 * @return {Function}
161
 * @api private
162
 */
163

164
exports.compileQueryParser = function compileQueryParser(val) {
8✔
165
  var fn;
166

167
  if (typeof val === 'function') {
7,622✔
168
    return val;
8✔
169
  }
170

171
  switch (val) {
7,614✔
172
    case true:
173
    case 'simple':
174
      fn = querystring.parse;
7,582✔
175
      break;
7,582✔
176
    case false:
177
      break;
8✔
178
    case 'extended':
179
      fn = parseExtendedQueryString;
16✔
180
      break;
16✔
181
    default:
182
      throw new TypeError('unknown value for query parser function: ' + val);
8✔
183
  }
184

185
  return fn;
7,606✔
186
}
187

188
/**
189
 * Compile "proxy trust" value to function.
190
 *
191
 * @param  {Boolean|String|Number|Array|Function} val
192
 * @return {Function}
193
 * @api private
194
 */
195

196
exports.compileTrust = function(val) {
8✔
197
  if (typeof val === 'function') return val;
7,814✔
198

199
  if (val === true) {
7,782✔
200
    // Support plain true/false
201
    return function(){ return true };
144✔
202
  }
203

204
  if (typeof val === 'number') {
7,646✔
205
    // Support trusting hop count
206
    return function(a, i){ return i < val };
96✔
207
  }
208

209
  if (typeof val === 'string') {
7,598✔
210
    // Support comma-separated values
211
    val = val.split(',')
32✔
212
      .map(function (v) { return v.trim() })
56✔
213
  }
214

215
  return proxyaddr.compile(val || []);
7,598✔
216
}
217

218
/**
219
 * Set the charset in a given Content-Type string.
220
 *
221
 * @param {String} type
222
 * @param {String} charset
223
 * @return {String}
224
 * @api private
225
 */
226

227
exports.setCharset = function setCharset(type, charset) {
8✔
228
  if (!type || !charset) {
5,184✔
229
    return type;
24✔
230
  }
231

232
  // parse type
233
  var parsed = contentType.parse(type);
5,160✔
234

235
  // set charset
236
  parsed.parameters.charset = charset;
5,160✔
237

238
  // format type
239
  return contentType.format(parsed);
5,160✔
240
};
241

242
/**
243
 * Create an ETag generator function, generating ETags with
244
 * the given options.
245
 *
246
 * @param {object} options
247
 * @return {function}
248
 * @private
249
 */
250

251
function createETagGenerator (options) {
252
  return function generateETag (body, encoding) {
16✔
253
    var buf = !Buffer.isBuffer(body)
5,160✔
254
      ? Buffer.from(body, encoding)
255
      : body
256

257
    return etag(buf, options)
5,160✔
258
  }
259
}
260

261
/**
262
 * Parse an extended query string with qs.
263
 *
264
 * @param {String} str
265
 * @return {Object}
266
 * @private
267
 */
268

269
function parseExtendedQueryString(str) {
270
  return qs.parse(str, {
16✔
271
    allowPrototypes: true
272
  });
273
}
274

275
/**
276
 * Load environment variables from .env file
277
 *
278
 * @param {String|Object} [envPath]
279
 * @param {Object} [options]
280
 * @param {Boolean} [options.override]
281
 * @param {String} [options.env]
282
 * @param {Boolean} [options.cascade]
283
 * @return {Object}v
284
 * @api private
285
 */
286

287
exports.loadEnv = function loadEnv(envPath, options) {
8✔
288

289
  if (typeof envPath === 'object' && envPath !== null && !Array.isArray(envPath)) {
168✔
290
    options = envPath;
48✔
291
    envPath = undefined;
48✔
292
  }
293

294
  options = options || {};
168✔
295
  const override = options.override === true;
168✔
296
  const cascade = options.cascade !== false; // Default to true
168✔
297
  const nodeEnv = options.env || process.env.NODE_ENV;
168✔
298

299
  var filesToLoad = [];
168✔
300
  var baseDir = process.cwd();
168✔
301

302
  // If specific path provided, use it
303
  if (envPath) {
168✔
304
    filesToLoad.push(path.resolve(envPath));
104✔
305
  } else {
306
    // Default behavior: load .env and optionally .env.[NODE_ENV]
307
    var baseEnvPath = path.resolve(baseDir, '.env');
64✔
308

309
    if (cascade) {
64✔
310
      // Load .env first, then .env.[environment]
311
      filesToLoad.push(baseEnvPath);
40✔
312

313
      if (nodeEnv) {
40✔
314
        filesToLoad.push(path.resolve(baseDir, '.env.' + nodeEnv));
40✔
315
      }
316
    } else if (nodeEnv) {
24✔
317
      // Only load .env.[environment]
318
      filesToLoad.push(path.resolve(baseDir, '.env.' + nodeEnv));
24✔
319
    } else {
320
      // Only load .env
NEW
321
      filesToLoad.push(baseEnvPath);
×
322
    }
323

324
    // Always try to load .env.local (for local overrides)
325
    var localEnvPath = path.resolve(baseDir, '.env.local');
64✔
326
    if (filesToLoad.indexOf(localEnvPath) === -1) {
64✔
327
      filesToLoad.push(localEnvPath);
64✔
328
    }
329
  }
330

331
  var allParsed = {};
168✔
332
  var loadedFiles = [];
168✔
333

334
  // Load files in order
335
  for (var i = 0; i < filesToLoad.length; i++) {
168✔
336
    var filePath = filesToLoad[i];
272✔
337

338
    if (!fs.existsSync(filePath)) {
272✔
339
      continue;
72✔
340
    }
341

342
    try {
200✔
343
      var content = fs.readFileSync(filePath, 'utf8');
200✔
344
      var parsed = parseEnvFile(content);
192✔
345

346
      // Merge parsed values
347
      Object.keys(parsed).forEach(function(key) {
192✔
348
        // Later files can override earlier ones in cascade mode
349
        if (!allParsed.hasOwnProperty(key) || cascade) {
264✔
350
          allParsed[key] = parsed[key];
264✔
351
        }
352
      });
353

354
      loadedFiles.push(filePath);
192✔
355
    } catch (err) {
356
      throw new Error('Failed to load .env file (' + filePath + '): ' + err.message);
8✔
357
    }
358
  }
359

360
  // Set environment variables
361
  Object.keys(allParsed).forEach(function(key) {
160✔
362
    if (override || !process.env.hasOwnProperty(key)) {
248✔
363
      process.env[key] = allParsed[key];
240✔
364
    }
365
  });
366

367
  // Add metadata about loaded files
368
  allParsed._loaded = loadedFiles;
160✔
369

370
  return allParsed;
160✔
371
};
372

373
/**
374
 * Parse .env file content
375
 *
376
 * @param {String} content - Content of .env file
377
 * @return {Object} Parsed key-value pairs
378
 * @api private
379
 */
380

381
function parseEnvFile(content) {
382
  const result = {};
192✔
383
  const lines = content.split('\n');
192✔
384

385
  for (let i = 0; i < lines.length; i++) {
192✔
386
    let line = lines[i].trim();
296✔
387

388
    // Skip empty lines and comments
389
    if (!line || line.startsWith('#')) {
296✔
390
      continue;
32✔
391
    }
392

393
    // Handle multi-line values
394
    while (line.endsWith('\\') && i < lines.length - 1) {
264✔
395
      line = line.slice(0, -1) + lines[++i].trim();
16✔
396
    }
397

398
    const equalsIndex = line.indexOf('=');
264✔
399
    if (equalsIndex === -1) {
264✔
NEW
400
      continue;
×
401
    }
402

403
    const key = line.slice(0, equalsIndex).trim();
264✔
404
    let value = line.slice(equalsIndex + 1).trim();
264✔
405

406
    // Remove quotes if present
407
    if ((value.startsWith('"') && value.endsWith('"')) ||
264✔
408
        (value.startsWith("'") && value.endsWith("'"))) {
409
      value = value.slice(1, -1);
40✔
410
      // Handle escaped characters in a single pass to avoid double-unescaping
411
      value = value.replace(/\\(.)/g, function(match, char) {
40✔
412
        switch (char) {
16✔
413
          case 'n': return '\n';
8✔
NEW
414
          case 'r': return '\r';
×
415
          case 't': return '\t';
8✔
NEW
416
          case '\\': return '\\';
×
NEW
417
          case '"': return '"';
×
NEW
418
          case "'": return "'";
×
NEW
419
          default: return match;
×
420
        }
421
      });
422
    }
423

424
    result[key] = value;
264✔
425
  }
426

427
  return result;
192✔
428
}
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