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

expressjs / express / 20291952576

17 Dec 2025 04:47AM UTC coverage: 98.32%. First build
20291952576

Pull #6952

github

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

108 of 120 new or added lines in 2 files covered. (90.0%)

878 of 893 relevant lines covered (98.32%)

507.33 hits per line

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

93.85
/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');
2✔
16
var contentType = require('content-type');
2✔
17
var etag = require('etag');
2✔
18
var mime = require('mime-types')
2✔
19
var proxyaddr = require('proxy-addr');
2✔
20
var qs = require('qs');
2✔
21
var querystring = require('node:querystring');
2✔
22
const { Buffer } = require('node:buffer');
2✔
23
const fs = require('node:fs');
2✔
24
const path = require('node:path');
2✔
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());
68✔
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 })
2✔
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 })
2✔
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){
2✔
64
  return ~type.indexOf('/')
196✔
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) {
2✔
78
  return types.map(exports.normalizeType);
10✔
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;
46✔
93
  var colonIndex = str.indexOf(';');
46✔
94
  var index = colonIndex === -1 ? length : colonIndex;
46✔
95
  var ret = { value: str.slice(0, index).trim(), quality: 1, params: {} };
46✔
96

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

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

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

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

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

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

121
  return ret;
46✔
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) {
2✔
133
  var fn;
134

135
  if (typeof val === 'function') {
1,916✔
136
    return val;
6✔
137
  }
138

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

153
  return fn;
1,908✔
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) {
2✔
165
  var fn;
166

167
  if (typeof val === 'function') {
1,902✔
168
    return val;
2✔
169
  }
170

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

185
  return fn;
1,898✔
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) {
2✔
197
  if (typeof val === 'function') return val;
1,950✔
198

199
  if (val === true) {
1,942✔
200
    // Support plain true/false
201
    return function(){ return true };
36✔
202
  }
203

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

209
  if (typeof val === 'string') {
1,896✔
210
    // Support comma-separated values
211
    val = val.split(',')
8✔
212
      .map(function (v) { return v.trim() })
14✔
213
  }
214

215
  return proxyaddr.compile(val || []);
1,896✔
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) {
2✔
228
  if (!type || !charset) {
1,294✔
229
    return type;
6✔
230
  }
231

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

235
  // set charset
236
  parsed.parameters.charset = charset;
1,288✔
237

238
  // format type
239
  return contentType.format(parsed);
1,288✔
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) {
4✔
253
    var buf = !Buffer.isBuffer(body)
1,288✔
254
      ? Buffer.from(body, encoding)
255
      : body
256

257
    return etag(buf, options)
1,288✔
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, {
4✔
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
 * @param {Boolean} [options.watch]  
284
 * @param {Function} [options.onChange] 
285
 * @param {Function} [options.onError] 
286
 * @return {Object|Function}
287
 * @api private
288
 */
289

290
exports.loadEnv = function loadEnv(envPath, options) {
2✔
291

292
  if (typeof envPath === 'object' && envPath !== null && !Array.isArray(envPath)) {
68✔
293
    options = envPath;
12✔
294
    envPath = undefined;
12✔
295
  }
296

297
  options = options || {};
68✔
298
  const override = options.override === true;
68✔
299
  const cascade = options.cascade !== false; // Default to true
68✔
300
  const nodeEnv = options.env || process.env.NODE_ENV;
68✔
301

302
  var filesToLoad = [];
68✔
303
  var baseDir = process.cwd();
68✔
304

305
  // If specific path provided, use it
306
  if (envPath) {
68✔
307
    filesToLoad.push(path.resolve(envPath));
52✔
308
  } else {
309
    // Default behavior: load .env and optionally .env.[NODE_ENV]
310
    var baseEnvPath = path.resolve(baseDir, '.env');
16✔
311

312
    if (cascade) {
16✔
313
      // Load .env first, then .env.[environment]
314
      filesToLoad.push(baseEnvPath);
10✔
315

316
      if (nodeEnv) {
10✔
317
        filesToLoad.push(path.resolve(baseDir, '.env.' + nodeEnv));
10✔
318
      }
319
    } else if (nodeEnv) {
6✔
320
      // Only load .env.[environment]
321
      filesToLoad.push(path.resolve(baseDir, '.env.' + nodeEnv));
6✔
322
    } else {
323
      // Only load .env
NEW
324
      filesToLoad.push(baseEnvPath);
×
325
    }
326

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

334
  var allParsed = {};
68✔
335
  var loadedFiles = [];
68✔
336

337
  // Load files in order
338
  for (var i = 0; i < filesToLoad.length; i++) {
68✔
339
    var filePath = filesToLoad[i];
94✔
340

341
    if (!fs.existsSync(filePath)) {
94✔
342
      continue;
18✔
343
    }
344

345
    try {
76✔
346
      var content = fs.readFileSync(filePath, 'utf8');
76✔
347
      var parsed = parseEnvFile(content);
74✔
348

349
      // Merge parsed values
350
      Object.keys(parsed).forEach(function(key) {
74✔
351
        // Later files can override earlier ones in cascade mode
352
        if (!allParsed.hasOwnProperty(key) || cascade) {
94✔
353
          allParsed[key] = parsed[key];
94✔
354
        }
355
      });
356

357
      loadedFiles.push(filePath);
74✔
358
    } catch (err) {
359
      throw new Error('Failed to load .env file (' + filePath + '): ' + err.message);
2✔
360
    }
361
  }
362

363
  // Set environment variables
364
  Object.keys(allParsed).forEach(function(key) {
66✔
365
    if (override || !process.env.hasOwnProperty(key)) {
90✔
366
      process.env[key] = allParsed[key];
88✔
367
    }
368
  });
369

370
  // Add metadata about loaded files
371
  allParsed._loaded = loadedFiles;
66✔
372

373
  // If watch option is enabled, set up file watchers
374
  if (options.watch === true) {
66✔
375
    var previousValues = {};
14✔
376
    var watchers = [];
14✔
377
    var isReloading = false;
14✔
378

379
    // Store current values (exclude metadata)
380
    Object.keys(allParsed).forEach(function(key) {
14✔
381
      if (key !== '_loaded') {
30✔
382
        previousValues[key] = allParsed[key];
16✔
383
      }
384
    });
385

386
    // Watch each loaded file
387
    loadedFiles.forEach(function(filePath) {
14✔
388
      try {
14✔
389
        var watcher = fs.watch(filePath, function(eventType) {
14✔
390
          if (eventType === 'change' && !isReloading) {
10✔
391
            isReloading = true;
10✔
392
            
393
            // Small delay to ensure file is fully written
394
            setTimeout(function() {
10✔
395
              try {
10✔
396
                // Reload with override to update existing values
397
                var reloaded = exports.loadEnv(envPath, Object.assign({}, options, {
10✔
398
                  override: true,
399
                  watch: false // Prevent recursive watching
400
                }));
401
                
402
                // Detect what changed
403
                var changed = {};
10✔
404
                var currentValues = {};
10✔
405
                
406
                Object.keys(reloaded).forEach(function(key) {
10✔
407
                  if (key !== '_loaded') {
20✔
408
                    currentValues[key] = reloaded[key];
10✔
409
                    
410
                    // Check if value changed
411
                    if (!previousValues.hasOwnProperty(key)) {
10✔
412
                      changed[key] = { type: 'added', value: reloaded[key] };
2✔
413
                    } else if (previousValues[key] !== reloaded[key]) {
8✔
414
                      changed[key] = { 
4✔
415
                        type: 'modified', 
416
                        oldValue: previousValues[key],
417
                        newValue: reloaded[key]
418
                      };
419
                    }
420
                  }
421
                });
422
                
423
                // Check for removed keys
424
                Object.keys(previousValues).forEach(function(key) {
10✔
425
                  if (!currentValues.hasOwnProperty(key)) {
12✔
426
                    changed[key] = { type: 'removed', oldValue: previousValues[key] };
4✔
427
                    // Remove from process.env if it was set by us
428
                    if (process.env[key] === previousValues[key]) {
4✔
429
                      delete process.env[key];
4✔
430
                    }
431
                  }
432
                });
433
                
434
                // Update previous values
435
                previousValues = currentValues;
10✔
436
                
437
                // Call onChange callback if there were changes
438
                if (Object.keys(changed).length > 0 && typeof options.onChange === 'function') {
10✔
439
                  options.onChange(changed, reloaded);
8✔
440
                }
441
                
442
              } catch (err) {
NEW
443
                if (typeof options.onError === 'function') {
×
NEW
444
                  options.onError(err);
×
445
                }
446
              } finally {
447
                isReloading = false;
10✔
448
              }
449
            }, 100);
450
          }
451
        });
452
        
453
        watchers.push(watcher);
14✔
454
      } catch (err) {
455
        // Silently ignore watch errors for individual files
NEW
456
        if (typeof options.onError === 'function') {
×
NEW
457
          options.onError(new Error('Failed to watch file: ' + filePath));
×
458
        }
459
      }
460
    });
461

462
    // Return unwatch function when watch is enabled
463
    return function unwatch() {
14✔
464
      watchers.forEach(function(watcher) {
14✔
465
        try {
14✔
466
          watcher.close();
14✔
467
        } catch (err) {
468
          // Ignore errors when closing watchers
469
        }
470
      });
471
      watchers = [];
14✔
472
    };
473
  }
474

475
  return allParsed;
52✔
476
};
477

478

479

480
/**
481
 * Parse .env file content
482
 *
483
 * @param {String} content - Content of .env file
484
 * @return {Object} Parsed key-value pairs
485
 * @api private
486
 */
487

488
function parseEnvFile(content) {
489
  const result = {};
74✔
490
  const lines = content.split('\n');
74✔
491

492
  for (let i = 0; i < lines.length; i++) {
74✔
493
    let line = lines[i].trim();
104✔
494

495
    if (!line || line.startsWith('#')) {
104✔
496
      continue;
8✔
497
    }
498

499
    if (line.startsWith('export ')) {
96✔
NEW
500
      line = line.slice(7).trim();
×
501
    }
502

503
    // Handle multi-line values
504
    while (line.endsWith('\\') && i < lines.length - 1) {
96✔
505
      line = line.slice(0, -1) + lines[++i].trim();
4✔
506
    }
507

508
    const equalsIndex = line.indexOf('=');
96✔
509
    if (equalsIndex === -1) {
96✔
510
      continue;
2✔
511
    }
512

513
    const key = line.slice(0, equalsIndex).trim();
94✔
514
    let value = line.slice(equalsIndex + 1).trim();
94✔
515

516
    // Handle inline comments for unquoted values
517
    if (!value.startsWith('"') && !value.startsWith("'")) {
94✔
518
      const commentIndex = value.indexOf('#');
84✔
519
      if (commentIndex !== -1) {
84✔
NEW
520
        value = value.slice(0, commentIndex).trim();
×
521
      }
522
    }
523

524
    // Remove quotes if present
525
    if ((value.startsWith('"') && value.endsWith('"')) ||
94✔
526
        (value.startsWith("'") && value.endsWith("'"))) {
527
      value = value.slice(1, -1);
10✔
528
      // Handle escaped characters in a single pass
529
      value = value.replace(/\\(.)/g, function(match, char) {
10✔
530
        switch (char) {
4✔
531
          case 'n': return '\n';
2✔
NEW
532
          case 'r': return '\r';
×
533
          case 't': return '\t';
2✔
NEW
534
          case '\\': return '\\';
×
NEW
535
          case '"': return '"';
×
NEW
536
          case "'": return "'";
×
NEW
537
          default: return match;
×
538
        }
539
      });
540
    }
541

542
    result[key] = value;
94✔
543
  }
544

545
  return result;
74✔
546
}
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