Skip to content

Commit a567b6f

Browse files
committed
feat(web-server): Allow Range headers in web server.
This does not support multiple-ranges. Closes #2140
1 parent ca4e2c7 commit a567b6f

5 files changed

Lines changed: 112 additions & 8 deletions

File tree

docs/config/02-files.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,15 @@ proxies: {
126126
},
127127
```
128128

129+
## Webserver features
130+
131+
* [Range requests][].
132+
* In-memory caching of files.
133+
* Watching for updates in the files.
134+
* Proxies to alter file paths.
135+
129136

130137
[glob]: https://github.com/isaacs/node-glob
131138
[preprocessors]: preprocessors.html
132139
[minimatch]: https://github.com/isaacs/minimatch
140+
[Range requests]: https://en.wikipedia.org/wiki/Byte_serving

lib/middleware/common.js

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,47 @@ var serve404 = function (response, path) {
2929
var createServeFile = function (fs, directory, config) {
3030
var cache = Object.create(null)
3131

32-
return function (filepath, response, transform, content, doNotCache) {
32+
return function (filepath, rangeHeader, response, transform, content, doNotCache) {
3333
var responseData
3434

35+
var convertForRangeRequest = function () {
36+
// If the header is invalid, ignore
37+
if (!rangeHeader.startsWith('bytes=')) {
38+
return 200
39+
}
40+
41+
responseData = new Buffer(responseData)
42+
43+
var ranges = rangeHeader.substr(6)
44+
if (ranges.indexOf(',') >= 0) {
45+
// Multiple ranges are not supported.
46+
responseData = new Buffer(0)
47+
return 416 // Requested range not satisfiable
48+
}
49+
var parts = /^([0-9]*)-([0-9]*)$/.exec(ranges)
50+
if (!parts || (!parts[1] && !parts[2])) {
51+
return 200
52+
}
53+
var start, end
54+
if (parts[1]) {
55+
start = Number(parts[1])
56+
end = parts[2] ? Number(parts[2]) : responseData.length
57+
} else {
58+
end = responseData.length
59+
start = responseData.length - Number(parts[2])
60+
}
61+
if (end <= start) {
62+
responseData = new Buffer(0)
63+
return 416 // Requested range not satisfiable
64+
}
65+
66+
response.setHeader(
67+
'Content-Range',
68+
'bytes ' + start + '-' + end + '/' + responseData.length)
69+
responseData = responseData.slice(start, end + 1)
70+
return 206
71+
}
72+
3573
if (directory) {
3674
filepath = directory + filepath
3775
}
@@ -57,7 +95,12 @@ var createServeFile = function (fs, directory, config) {
5795
// call custom transform fn to transform the data
5896
responseData = transform && transform(content) || content
5997

60-
response.writeHead(200)
98+
if (rangeHeader) {
99+
var code = convertForRangeRequest()
100+
response.writeHead(code)
101+
} else {
102+
response.writeHead(200)
103+
}
61104

62105
log.debug('serving (cached): ' + filepath)
63106
return response.end(responseData)
@@ -77,7 +120,12 @@ var createServeFile = function (fs, directory, config) {
77120
// call custom transform fn to transform the data
78121
responseData = transform && transform(data.toString()) || data
79122

80-
response.writeHead(200)
123+
if (rangeHeader) {
124+
var code = convertForRangeRequest()
125+
response.writeHead(code)
126+
} else {
127+
response.writeHead(200)
128+
}
81129

82130
log.debug('serving: ' + filepath)
83131
return response.end(responseData)

lib/middleware/karma.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ var createKarmaMiddleware = function (
8989
var jsVersion = injector.get('config.jsVersion')
9090

9191
var requestUrl = request.normalizedUrl.replace(/\?.*/, '')
92+
var requestedRangeHeader = request.headers['range']
9293

9394
// redirect /__karma__ to /__karma__ (trailing slash)
9495
if (requestUrl === urlRoot.substr(0, urlRoot.length - 1)) {
@@ -107,7 +108,7 @@ var createKarmaMiddleware = function (
107108

108109
// serve client.html
109110
if (requestUrl === '/') {
110-
return serveStaticFile('/client.html', response, function (data) {
111+
return serveStaticFile('/client.html', requestedRangeHeader, response, function (data) {
111112
return data
112113
.replace('\n%X_UA_COMPATIBLE%', getXUACompatibleMetaElement(request.url))
113114
.replace('%X_UA_COMPATIBLE_URL%', getXUACompatibleUrl(request.url))
@@ -118,15 +119,15 @@ var createKarmaMiddleware = function (
118119
var jsFiles = ['/karma.js', '/context.js', '/debug.js']
119120
var isRequestingJsFile = jsFiles.indexOf(requestUrl) !== -1
120121
if (isRequestingJsFile) {
121-
return serveStaticFile(requestUrl, response, function (data) {
122+
return serveStaticFile(requestUrl, requestedRangeHeader, response, function (data) {
122123
return data.replace('%KARMA_URL_ROOT%', urlRoot)
123124
.replace('%KARMA_VERSION%', VERSION)
124125
})
125126
}
126127

127128
// serve the favicon
128129
if (requestUrl === '/favicon.ico') {
129-
return serveStaticFile(requestUrl, response)
130+
return serveStaticFile(requestUrl, requestedRangeHeader, response)
130131
}
131132

132133
// serve context.html - execution context within the iframe
@@ -152,7 +153,7 @@ var createKarmaMiddleware = function (
152153
requestedFileUrl = requestUrl
153154
}
154155

155-
fileServer(requestedFileUrl, response, function (data) {
156+
fileServer(requestedFileUrl, requestedRangeHeader, response, function (data) {
156157
common.setNoCacheHeaders(response)
157158

158159
var scriptTags = files.included.map(function (file) {

lib/middleware/source_files.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ var createSourceFilesMiddleware = function (filesPromise, serveFile, basePath, u
3030
return filesPromise.then(function (files) {
3131
// TODO(vojta): change served to be a map rather then an array
3232
var file = findByPath(files.served, requestedFilePath)
33+
var rangeHeader = request.headers['range']
3334

3435
if (file) {
35-
serveFile(file.contentPath || file.path, response, function () {
36+
serveFile(file.contentPath || file.path, rangeHeader, response, function () {
3637
if (/\?\w+/.test(request.url)) {
3738
// files with timestamps - cache one year, rely on timestamps
3839
common.setHeavyCacheHeaders(response)

test/unit/middleware/source_files.spec.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,52 @@ describe('middleware.source_files', function () {
5959
return files.resolve({included: [], served: list})
6060
}
6161

62+
describe('Range headers', function () {
63+
beforeEach(function () {
64+
servedFiles([
65+
new File('/src/some.js')
66+
])
67+
})
68+
69+
it('allows single explicit ranges', function () {
70+
return request(server)
71+
.get('/absolute/src/some.js')
72+
.set('Range', 'bytes=3-6')
73+
.expect('Content-Range', 'bytes 3-6/9')
74+
.expect(206, 'sour')
75+
})
76+
77+
it('allows single range with no end', function () {
78+
return request(server)
79+
.get('/absolute/src/some.js')
80+
.set('Range', 'bytes=3-')
81+
.expect('Content-Range', 'bytes 3-9/9')
82+
.expect(206, 'source')
83+
})
84+
85+
it('allows single range with suffix', function () {
86+
return request(server)
87+
.get('/absolute/src/some.js')
88+
.set('Range', 'bytes=-5')
89+
.expect('Content-Range', 'bytes 4-9/9')
90+
.expect(206, 'ource')
91+
})
92+
93+
it('doesn\'t support multiple ranges', function () {
94+
return request(server)
95+
.get('/absolute/src/some.js')
96+
.set('Range', 'bytes=0-2,-3')
97+
.expect(416, '')
98+
})
99+
100+
it('will return 416', function () {
101+
return request(server)
102+
.get('/absolute/src/some.js')
103+
.set('Range', 'bytes=20-')
104+
.expect(416, '')
105+
})
106+
})
107+
62108
it('should serve absolute js source files ignoring timestamp', function () {
63109
servedFiles([
64110
new File('/src/some.js')

0 commit comments

Comments
 (0)