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

expressjs / express / 14713641238

28 Apr 2025 05:10PM UTC coverage: 85.604% (-14.4%) from 100.0%
14713641238

Pull #6487

github

web-flow
Merge 4bf9acc9c into f9954dd31
Pull Request #6487: feat(http2): add HTTP/2 support

9 of 139 new or added lines in 2 files covered. (6.47%)

773 of 903 relevant lines covered (85.6%)

1502.24 hits per line

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

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

9
'use strict';
10

11
/**
12
 * Module dependencies.
13
 * @private
14
 */
15

16
var http2 = require('node:http2');
6✔
17
var debug = require('debug')('express:http2');
6✔
18

19
/**
20
 * HTTP/2 constants
21
 */
22
const {
23
  HTTP2_HEADER_METHOD,
24
  HTTP2_HEADER_PATH,
25
  HTTP2_HEADER_STATUS,
26
  HTTP2_HEADER_CONTENT_TYPE
27
} = http2.constants;
6✔
28

29
/**
30
 * Create an HTTP/2 server for an Express app
31
 *
32
 * @param {Object} app Express application
33
 * @param {Object} options HTTP/2 server options
34
 * @return {Object} HTTP/2 server instance
35
 * @public
36
 */
37
exports.createServer = function createServer(app, options) {
6✔
NEW
38
  debug('Creating HTTP/2 server');
×
39

40
  // Create the HTTP/2 server
NEW
41
  const server = http2.createServer(options);
×
42

43
  // Handle HTTP/2 streams
NEW
44
  server.on('stream', (stream, headers) => {
×
NEW
45
    debug('Received HTTP/2 stream');
×
46

47
    // Create mock request and response objects compatible with Express
NEW
48
    const req = createRequest(stream, headers);
×
NEW
49
    const res = createResponse(stream);
×
50

51
    // Set req and res references to each other
NEW
52
    req.res = res;
×
NEW
53
    res.req = req;
×
54

55
    // Set app as req.app and res.app
NEW
56
    req.app = res.app = app;
×
57

58
    // Create a custom finalhandler that works with HTTP/2
NEW
59
    const done = function(err) {
×
NEW
60
      if (err && !stream.destroyed) {
×
NEW
61
        const statusCode = err.status || err.statusCode || 500;
×
NEW
62
        stream.respond({
×
63
          [HTTP2_HEADER_STATUS]: statusCode,
64
          [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
65
        });
NEW
66
        stream.end(statusCode === 500 ? 'Internal Server Error' : err.message);
×
NEW
67
      } else if (!res.headersSent && !stream.destroyed) {
×
68
        // Default 404 handler
NEW
69
        stream.respond({
×
70
          [HTTP2_HEADER_STATUS]: 404,
71
          [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
72
        });
NEW
73
        stream.end('Not Found');
×
74
      }
75
    };
76

77
    // Handle the request with Express
NEW
78
    app(req, res, done);
×
79
  });
80

NEW
81
  return server;
×
82
};
83

84
/**
85
 * Create an HTTP/2 secure server for an Express app
86
 *
87
 * @param {Object} app Express application
88
 * @param {Object} options HTTP/2 secure server options
89
 * @return {Object} HTTP/2 secure server instance
90
 * @public
91
 */
92
exports.createSecureServer = function createSecureServer(app, options) {
6✔
NEW
93
  debug('Creating HTTP/2 secure server');
×
94

NEW
95
  if (!options.key || !options.cert) {
×
NEW
96
    throw new Error('HTTP/2 secure server requires key and cert options');
×
97
  }
98

99
  // Create the HTTP/2 secure server
NEW
100
  const server = http2.createSecureServer(options);
×
101

102
  // Handle HTTP/2 streams
NEW
103
  server.on('stream', (stream, headers) => {
×
NEW
104
    debug('Received HTTP/2 secure stream');
×
105

106
    // Create mock request and response objects compatible with Express
NEW
107
    const req = createRequest(stream, headers);
×
NEW
108
    const res = createResponse(stream);
×
109

110
    // Set req and res references to each other
NEW
111
    req.res = res;
×
NEW
112
    res.req = req;
×
113

114
    // Set app as req.app and res.app
NEW
115
    req.app = res.app = app;
×
116

117
    // Create a custom finalhandler that works with HTTP/2
NEW
118
    const done = function(err) {
×
NEW
119
      if (err && !stream.destroyed) {
×
NEW
120
        const statusCode = err.status || err.statusCode || 500;
×
NEW
121
        stream.respond({
×
122
          [HTTP2_HEADER_STATUS]: statusCode,
123
          [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
124
        });
NEW
125
        stream.end(statusCode === 500 ? 'Internal Server Error' : err.message);
×
NEW
126
      } else if (!res.headersSent && !stream.destroyed) {
×
127
        // Default 404 handler
NEW
128
        stream.respond({
×
129
          [HTTP2_HEADER_STATUS]: 404,
130
          [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
131
        });
NEW
132
        stream.end('Not Found');
×
133
      }
134
    };
135

136
    // Handle the request with Express
NEW
137
    app(req, res, done);
×
138
  });
139

NEW
140
  return server;
×
141
};
142

143
/**
144
 * Create a mock request object compatible with Express
145
 *
146
 * @param {Object} stream HTTP/2 stream
147
 * @param {Object} headers HTTP/2 headers
148
 * @return {Object} Express-compatible request object
149
 * @private
150
 */
151
function createRequest(stream, headers) {
NEW
152
  const method = headers[HTTP2_HEADER_METHOD] || 'GET';
×
NEW
153
  const url = headers[HTTP2_HEADER_PATH] || '/';
×
154

NEW
155
  debug(`HTTP/2 ${method} ${url}`);
×
156

157
  // Create basic request object
NEW
158
  const req = {
×
159
    stream: stream,
160
    httpVersionMajor: 2,
161
    httpVersionMinor: 0,
162
    httpVersion: '2.0',
163
    complete: false,
164
    headers: Object.assign({}, headers),
165
    rawHeaders: [],
166
    trailers: {},
167
    rawTrailers: [],
168
    aborted: false,
169
    upgrade: false,
170
    url: url,
171
    method: method,
172
    statusCode: null,
173
    statusMessage: null,
174
    socket: stream.session.socket,
175
    connection: stream.session.socket,
176

177
    // Body parsing
178
    body: {},
179

180
    // Express extensions
181
    params: {},
182
    query: {},
183
    res: null,
184

185
    // Properties needed by finalhandler
186
    _readableState: { pipes: null },
NEW
187
    unpipe: function() { return this; },
×
NEW
188
    removeListener: function() { return this; },
×
189

190
    // Stream-like methods
191
    on: stream.on.bind(stream),
192
    once: stream.once.bind(stream),
NEW
193
    pipe: function() { return this; },
×
194
    addListener: function(type, listener) {
NEW
195
      stream.on(type, listener);
×
NEW
196
      return this;
×
197
    },
198
    removeAllListeners: function() {
NEW
199
      return this;
×
200
    },
201
    emit: stream.emit.bind(stream),
202
    setEncoding: function(encoding) {
NEW
203
      stream.setEncoding(encoding);
×
NEW
204
      return this;
×
205
    },
206
    pause: function() {
NEW
207
      stream.pause && stream.pause();
×
NEW
208
      return this;
×
209
    },
210
    resume: function() {
NEW
211
      stream.resume && stream.resume();
×
NEW
212
      return this;
×
213
    },
214

215
    // Request reading methods
NEW
216
    read: stream.read ? stream.read.bind(stream) : () => null,
×
217
  };
218

219
  // Parse URL parts
NEW
220
  parseUrl(req);
×
221

222
  // Add data event handling
NEW
223
  const chunks = [];
×
224

NEW
225
  stream.on('data', chunk => {
×
NEW
226
    chunks.push(chunk);
×
227
  });
228

NEW
229
  stream.on('end', () => {
×
NEW
230
    req.complete = true;
×
NEW
231
    req.rawBody = Buffer.concat(chunks);
×
232

233
    // Try to parse as JSON if content-type is application/json
NEW
234
    if (req.headers['content-type'] === 'application/json') {
×
NEW
235
      try {
×
NEW
236
        req.body = JSON.parse(req.rawBody.toString());
×
237
      } catch (e) {
NEW
238
        debug('Failed to parse request body as JSON', e);
×
239
      }
240
    }
241
  });
242

NEW
243
  return req;
×
244
}
245

246
/**
247
 * Create a mock response object compatible with Express
248
 *
249
 * @param {Object} stream HTTP/2 stream
250
 * @return {Object} Express-compatible response object
251
 * @private
252
 */
253
function createResponse(stream) {
NEW
254
  const res = {
×
255
    stream: stream,
256
    headersSent: false,
257
    finished: false,
258
    statusCode: 200,
259
    locals: Object.create(null),
260
    req: null,
261

262
    // Properties needed by finalhandler
263
    _header: '',
264
    _implicitHeader: function() {},
265
    connection: { writable: true },
266

267
    // Response methods
268
    status: function(code) {
NEW
269
      this.statusCode = code;
×
NEW
270
      return this;
×
271
    },
272

273
    send: function(body) {
NEW
274
      if (this.headersSent) {
×
NEW
275
        debug('Headers already sent, ignoring send');
×
NEW
276
        return this;
×
277
      }
278

NEW
279
      if (!this.getHeader('Content-Type')) {
×
NEW
280
        if (typeof body === 'string') {
×
NEW
281
          this.setHeader('Content-Type', 'text/html');
×
NEW
282
        } else if (Buffer.isBuffer(body)) {
×
NEW
283
          this.setHeader('Content-Type', 'application/octet-stream');
×
NEW
284
        } else if (typeof body === 'object') {
×
NEW
285
          this.setHeader('Content-Type', 'application/json');
×
NEW
286
          body = JSON.stringify(body);
×
287
        }
288
      }
289

NEW
290
      const headers = {
×
291
        [HTTP2_HEADER_STATUS]: this.statusCode,
292
      };
293

294
      // Convert traditional headers to HTTP/2 headers
NEW
295
      for (const name in this._headers) {
×
NEW
296
        headers[name.toLowerCase()] = this._headers[name];
×
297
      }
298

NEW
299
      stream.respond(headers);
×
NEW
300
      this.headersSent = true;
×
301

NEW
302
      if (body !== undefined) {
×
NEW
303
        stream.end(body);
×
NEW
304
        this.finished = true;
×
305
      }
306

NEW
307
      return this;
×
308
    },
309

310
    json: function(obj) {
NEW
311
      if (!this.getHeader('Content-Type')) {
×
NEW
312
        this.setHeader('Content-Type', 'application/json');
×
313
      }
314

NEW
315
      return this.send(JSON.stringify(obj));
×
316
    },
317

318
    sendStatus: function(code) {
NEW
319
      const status = code.toString();
×
NEW
320
      this.statusCode = code;
×
NEW
321
      return this.send(status);
×
322
    },
323

324
    // Response streaming methods
325
    write: function(data, encoding) {
NEW
326
      if (!this.headersSent) {
×
NEW
327
        const headers = {
×
328
          [HTTP2_HEADER_STATUS]: this.statusCode,
329
        };
330

331
        // Convert traditional headers to HTTP/2 headers
NEW
332
        for (const name in this._headers) {
×
NEW
333
          headers[name.toLowerCase()] = this._headers[name];
×
334
        }
335

NEW
336
        stream.respond(headers);
×
NEW
337
        this.headersSent = true;
×
338
      }
339

NEW
340
      return stream.write(data, encoding);
×
341
    },
342

343
    // Headers management
344
    _headers: {},
345
    getHeaders: function() {
NEW
346
      return Object.assign({}, this._headers);
×
347
    },
348

349
    setHeader: function(name, value) {
NEW
350
      this._headers[name] = value;
×
NEW
351
      return this;
×
352
    },
353

354
    getHeader: function(name) {
NEW
355
      return this._headers[name];
×
356
    },
357

358
    removeHeader: function(name) {
NEW
359
      delete this._headers[name];
×
NEW
360
      return this;
×
361
    },
362

363
    end: function(data) {
NEW
364
      if (this.finished) {
×
NEW
365
        debug('Response already finished, ignoring end');
×
NEW
366
        return this;
×
367
      }
368

NEW
369
      if (!this.headersSent) {
×
NEW
370
        const headers = {
×
371
          [HTTP2_HEADER_STATUS]: this.statusCode,
372
        };
373

374
        // Convert traditional headers to HTTP/2 headers
NEW
375
        for (const name in this._headers) {
×
NEW
376
          headers[name.toLowerCase()] = this._headers[name];
×
377
        }
378

NEW
379
        stream.respond(headers);
×
NEW
380
        this.headersSent = true;
×
381
      }
382

NEW
383
      stream.end(data);
×
NEW
384
      this.finished = true;
×
NEW
385
      return this;
×
386
    }
387
  };
388

NEW
389
  return res;
×
390
}
391

392
/**
393
 * Parse URL parts from request
394
 *
395
 * @param {Object} req Request object
396
 * @private
397
 */
398
function parseUrl(req) {
NEW
399
  const url = new URL(req.url, 'http://localhost');
×
400

NEW
401
  req.path = url.pathname;
×
NEW
402
  req.hostname = url.hostname;
×
403

404
  // Parse query string
NEW
405
  req.query = {};
×
NEW
406
  for (const [key, value] of url.searchParams.entries()) {
×
NEW
407
    req.query[key] = value;
×
408
  }
409
}
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