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

expressjs / express / 15122266000

19 May 2025 08:12PM UTC coverage: 85.604% (-14.4%) from 100.0%
15122266000

Pull #6487

github

web-flow
Merge da8cdf8cf into 9f4dbe3a1
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%)

500.27 hits per line

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

3.7
/lib/http2.js
1
'use strict';
2

3
/**
4
 * Module dependencies.
5
 * @private
6
 */
7

8
var http2 = require('node:http2');
2✔
9
var debug = require('debug')('express:http2');
2✔
10

11
/**
12
 * HTTP/2 constants
13
 */
14
const {
15
  HTTP2_HEADER_METHOD,
16
  HTTP2_HEADER_PATH,
17
  HTTP2_HEADER_STATUS,
18
  HTTP2_HEADER_CONTENT_TYPE
19
} = http2.constants;
2✔
20

21
/**
22
 * Create an HTTP/2 server for an Express app
23
 *
24
 * @param {Object} app Express application
25
 * @param {Object} options HTTP/2 server options
26
 * @return {Object} HTTP/2 server instance
27
 * @public
28
 */
29
exports.createServer = function createServer(app, options) {
2✔
NEW
30
  debug('Creating HTTP/2 server');
×
31

32
  // Create the HTTP/2 server
NEW
33
  const server = http2.createServer(options);
×
34

35
  // Handle HTTP/2 streams
NEW
36
  server.on('stream', (stream, headers) => {
×
NEW
37
    debug('Received HTTP/2 stream');
×
38

39
    // Create mock request and response objects compatible with Express
NEW
40
    const req = createRequest(stream, headers);
×
NEW
41
    const res = createResponse(stream);
×
42

43
    // Set req and res references to each other
NEW
44
    req.res = res;
×
NEW
45
    res.req = req;
×
46

47
    // Set app as req.app and res.app
NEW
48
    req.app = res.app = app;
×
49

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

69
    // Handle the request with Express
NEW
70
    app(req, res, done);
×
71
  });
72

NEW
73
  return server;
×
74
};
75

76
/**
77
 * Create an HTTP/2 secure server for an Express app
78
 *
79
 * @param {Object} app Express application
80
 * @param {Object} options HTTP/2 secure server options
81
 * @return {Object} HTTP/2 secure server instance
82
 * @public
83
 */
84
exports.createSecureServer = function createSecureServer(app, options) {
2✔
NEW
85
  debug('Creating HTTP/2 secure server');
×
86

NEW
87
  if (!options.key || !options.cert) {
×
NEW
88
    throw new Error('HTTP/2 secure server requires key and cert options');
×
89
  }
90

91
  // Create the HTTP/2 secure server
NEW
92
  const server = http2.createSecureServer(options);
×
93

94
  // Handle HTTP/2 streams
NEW
95
  server.on('stream', (stream, headers) => {
×
NEW
96
    debug('Received HTTP/2 secure stream');
×
97

98
    // Create mock request and response objects compatible with Express
NEW
99
    const req = createRequest(stream, headers);
×
NEW
100
    const res = createResponse(stream);
×
101

102
    // Set req and res references to each other
NEW
103
    req.res = res;
×
NEW
104
    res.req = req;
×
105

106
    // Set app as req.app and res.app
NEW
107
    req.app = res.app = app;
×
108

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

128
    // Handle the request with Express
NEW
129
    app(req, res, done);
×
130
  });
131

NEW
132
  return server;
×
133
};
134

135
/**
136
 * Create a mock request object compatible with Express
137
 *
138
 * @param {Object} stream HTTP/2 stream
139
 * @param {Object} headers HTTP/2 headers
140
 * @return {Object} Express-compatible request object
141
 * @private
142
 */
143
function createRequest(stream, headers) {
NEW
144
  const method = headers[HTTP2_HEADER_METHOD] || 'GET';
×
NEW
145
  const url = headers[HTTP2_HEADER_PATH] || '/';
×
146

NEW
147
  debug(`HTTP/2 ${method} ${url}`);
×
148

149
  // Create basic request object
NEW
150
  const req = {
×
151
    stream: stream,
152
    httpVersionMajor: 2,
153
    httpVersionMinor: 0,
154
    httpVersion: '2.0',
155
    complete: false,
156
    headers: Object.assign({}, headers),
157
    rawHeaders: [],
158
    trailers: {},
159
    rawTrailers: [],
160
    aborted: false,
161
    upgrade: false,
162
    url: url,
163
    method: method,
164
    statusCode: null,
165
    statusMessage: null,
166
    socket: stream.session.socket,
167
    connection: stream.session.socket,
168

169
    // Body parsing
170
    body: {},
171

172
    // Express extensions
173
    params: {},
174
    query: {},
175
    res: null,
176

177
    // Properties needed by finalhandler
178
    _readableState: { pipes: null },
NEW
179
    unpipe: function() { return this; },
×
NEW
180
    removeListener: function() { return this; },
×
181

182
    // Stream-like methods
183
    on: stream.on.bind(stream),
184
    once: stream.once.bind(stream),
NEW
185
    pipe: function() { return this; },
×
186
    addListener: function(type, listener) {
NEW
187
      stream.on(type, listener);
×
NEW
188
      return this;
×
189
    },
190
    removeAllListeners: function() {
NEW
191
      return this;
×
192
    },
193
    emit: stream.emit.bind(stream),
194
    setEncoding: function(encoding) {
NEW
195
      stream.setEncoding(encoding);
×
NEW
196
      return this;
×
197
    },
198
    pause: function() {
NEW
199
      stream.pause && stream.pause();
×
NEW
200
      return this;
×
201
    },
202
    resume: function() {
NEW
203
      stream.resume && stream.resume();
×
NEW
204
      return this;
×
205
    },
206

207
    // Request reading methods
NEW
208
    read: stream.read ? stream.read.bind(stream) : () => null,
×
209
  };
210

211
  // Parse URL parts
NEW
212
  parseUrl(req);
×
213

214
  // Add data event handling
NEW
215
  const chunks = [];
×
216

NEW
217
  stream.on('data', chunk => {
×
NEW
218
    chunks.push(chunk);
×
219
  });
220

NEW
221
  stream.on('end', () => {
×
NEW
222
    req.complete = true;
×
NEW
223
    req.rawBody = Buffer.concat(chunks);
×
224

225
    // Try to parse as JSON if content-type is application/json
NEW
226
    if (req.headers['content-type'] === 'application/json') {
×
NEW
227
      try {
×
NEW
228
        req.body = JSON.parse(req.rawBody.toString());
×
229
      } catch (e) {
NEW
230
        debug('Failed to parse request body as JSON', e);
×
231
      }
232
    }
233
  });
234

NEW
235
  return req;
×
236
}
237

238
/**
239
 * Create a mock response object compatible with Express
240
 *
241
 * @param {Object} stream HTTP/2 stream
242
 * @return {Object} Express-compatible response object
243
 * @private
244
 */
245
function createResponse(stream) {
NEW
246
  const res = {
×
247
    stream: stream,
248
    headersSent: false,
249
    finished: false,
250
    statusCode: 200,
251
    locals: Object.create(null),
252
    req: null,
253

254
    // Properties needed by finalhandler
255
    _header: '',
256
    _implicitHeader: function() {},
257
    connection: { writable: true },
258

259
    // Response methods
260
    status: function(code) {
NEW
261
      this.statusCode = code;
×
NEW
262
      return this;
×
263
    },
264

265
    send: function(body) {
NEW
266
      if (this.headersSent) {
×
NEW
267
        debug('Headers already sent, ignoring send');
×
NEW
268
        return this;
×
269
      }
270

NEW
271
      if (!this.getHeader('Content-Type')) {
×
NEW
272
        if (typeof body === 'string') {
×
NEW
273
          this.setHeader('Content-Type', 'text/html');
×
NEW
274
        } else if (Buffer.isBuffer(body)) {
×
NEW
275
          this.setHeader('Content-Type', 'application/octet-stream');
×
NEW
276
        } else if (typeof body === 'object') {
×
NEW
277
          this.setHeader('Content-Type', 'application/json');
×
NEW
278
          body = JSON.stringify(body);
×
279
        }
280
      }
281

NEW
282
      const headers = {
×
283
        [HTTP2_HEADER_STATUS]: this.statusCode,
284
      };
285

286
      // Convert traditional headers to HTTP/2 headers
NEW
287
      for (const name in this._headers) {
×
NEW
288
        headers[name.toLowerCase()] = this._headers[name];
×
289
      }
290

NEW
291
      stream.respond(headers);
×
NEW
292
      this.headersSent = true;
×
293

NEW
294
      if (body !== undefined) {
×
NEW
295
        stream.end(body);
×
NEW
296
        this.finished = true;
×
297
      }
298

NEW
299
      return this;
×
300
    },
301

302
    json: function(obj) {
NEW
303
      if (!this.getHeader('Content-Type')) {
×
NEW
304
        this.setHeader('Content-Type', 'application/json');
×
305
      }
306

NEW
307
      return this.send(JSON.stringify(obj));
×
308
    },
309

310
    sendStatus: function(code) {
NEW
311
      const status = code.toString();
×
NEW
312
      this.statusCode = code;
×
NEW
313
      return this.send(status);
×
314
    },
315

316
    // Response streaming methods
317
    write: function(data, encoding) {
NEW
318
      if (!this.headersSent) {
×
NEW
319
        const headers = {
×
320
          [HTTP2_HEADER_STATUS]: this.statusCode,
321
        };
322

323
        // Convert traditional headers to HTTP/2 headers
NEW
324
        for (const name in this._headers) {
×
NEW
325
          headers[name.toLowerCase()] = this._headers[name];
×
326
        }
327

NEW
328
        stream.respond(headers);
×
NEW
329
        this.headersSent = true;
×
330
      }
331

NEW
332
      return stream.write(data, encoding);
×
333
    },
334

335
    // Headers management
336
    _headers: {},
337
    getHeaders: function() {
NEW
338
      return Object.assign({}, this._headers);
×
339
    },
340

341
    setHeader: function(name, value) {
NEW
342
      this._headers[name] = value;
×
NEW
343
      return this;
×
344
    },
345

346
    getHeader: function(name) {
NEW
347
      return this._headers[name];
×
348
    },
349

350
    removeHeader: function(name) {
NEW
351
      delete this._headers[name];
×
NEW
352
      return this;
×
353
    },
354

355
    end: function(data) {
NEW
356
      if (this.finished) {
×
NEW
357
        debug('Response already finished, ignoring end');
×
NEW
358
        return this;
×
359
      }
360

NEW
361
      if (!this.headersSent) {
×
NEW
362
        const headers = {
×
363
          [HTTP2_HEADER_STATUS]: this.statusCode,
364
        };
365

366
        // Convert traditional headers to HTTP/2 headers
NEW
367
        for (const name in this._headers) {
×
NEW
368
          headers[name.toLowerCase()] = this._headers[name];
×
369
        }
370

NEW
371
        stream.respond(headers);
×
NEW
372
        this.headersSent = true;
×
373
      }
374

NEW
375
      stream.end(data);
×
NEW
376
      this.finished = true;
×
NEW
377
      return this;
×
378
    }
379
  };
380

NEW
381
  return res;
×
382
}
383

384
/**
385
 * Parse URL parts from request
386
 *
387
 * @param {Object} req Request object
388
 * @private
389
 */
390
function parseUrl(req) {
NEW
391
  const url = new URL(req.url, 'http://localhost');
×
392

NEW
393
  req.path = url.pathname;
×
NEW
394
  req.hostname = url.hostname;
×
395

396
  // Parse query string
NEW
397
  req.query = {};
×
NEW
398
  for (const [key, value] of url.searchParams.entries()) {
×
NEW
399
    req.query[key] = value;
×
400
  }
401
}
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