Skip to content

Commit

Permalink
fix #2851: implement HTTP HEAD requests
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jan 19, 2023
1 parent 7c0526e commit f48391a
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 28 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

* Implement HTTP `HEAD` requests in serve mode ([#2851](https://github.com/evanw/esbuild/issues/2851))

Previously esbuild's serve mode only responded to HTTP `GET` requests. With this release, esbuild's serve mode will also respond to HTTP `HEAD` requests, which are just like HTTP `GET` requests except that the body of the response is omitted.

## 0.17.3

* Fix incorrect CSS minification for certain rules ([#2838](https://github.com/evanw/esbuild/issues/2838))
Expand Down
31 changes: 19 additions & 12 deletions pkg/api/serve_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,15 @@ func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
return
}

// Handle get requests
if req.Method == "GET" && strings.HasPrefix(req.URL.Path, "/") {
// HEAD requests omit the body
maybeWriteResponseBody := func(bytes []byte) { res.Write(bytes) }
isHEAD := req.Method == "HEAD"
if isHEAD {
maybeWriteResponseBody = func(bytes []byte) { res.Write(nil) }
}

// Handle GET and HEAD requests
if (isHEAD || req.Method == "GET") && strings.HasPrefix(req.URL.Path, "/") {
res.Header().Set("Access-Control-Allow-Origin", "*")
queryPath := path.Clean(req.URL.Path)[1:]
result := h.rebuild()
Expand All @@ -136,7 +143,7 @@ func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "text/plain; charset=utf-8")
go h.notifyRequest(time.Since(start), req, http.StatusServiceUnavailable)
res.WriteHeader(http.StatusServiceUnavailable)
res.Write([]byte(errorsToString(result.Errors)))
maybeWriteResponseBody([]byte(errorsToString(result.Errors)))
return
}

Expand Down Expand Up @@ -186,7 +193,7 @@ func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
// insensitive check because some file systems are case-sensitive.
go h.notifyRequest(time.Since(start), req, http.StatusForbidden)
res.WriteHeader(http.StatusForbidden)
res.Write([]byte("403 - Forbidden"))
maybeWriteResponseBody([]byte("403 - Forbidden"))
return
}
}
Expand All @@ -197,7 +204,7 @@ func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
} else if err != syscall.ENOENT {
go h.notifyRequest(time.Since(start), req, http.StatusInternalServerError)
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(fmt.Sprintf("500 - Internal server error: %s", err.Error())))
maybeWriteResponseBody([]byte(fmt.Sprintf("500 - Internal server error: %s", err.Error())))
return
}
}
Expand Down Expand Up @@ -225,7 +232,7 @@ func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
} else if err != syscall.ENOENT {
go h.notifyRequest(time.Since(start), req, http.StatusInternalServerError)
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(fmt.Sprintf("500 - Internal server error: %s", err.Error())))
maybeWriteResponseBody([]byte(fmt.Sprintf("500 - Internal server error: %s", err.Error())))
return
}
}
Expand All @@ -235,7 +242,7 @@ func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Location", req.URL.Path+"/")
go h.notifyRequest(time.Since(start), req, http.StatusFound)
res.WriteHeader(http.StatusFound)
res.Write(nil)
maybeWriteResponseBody(nil)
return
}

Expand All @@ -249,7 +256,7 @@ func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
} else if err != syscall.ENOENT {
go h.notifyRequest(time.Since(start), req, http.StatusInternalServerError)
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(fmt.Sprintf("500 - Internal server error: %s", err.Error())))
maybeWriteResponseBody([]byte(fmt.Sprintf("500 - Internal server error: %s", err.Error())))
return
}
}
Expand Down Expand Up @@ -277,7 +284,7 @@ func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
if err != nil {
go h.notifyRequest(time.Since(start), req, http.StatusInternalServerError)
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(fmt.Sprintf("500 - Internal server error: %s", err.Error())))
maybeWriteResponseBody([]byte(fmt.Sprintf("500 - Internal server error: %s", err.Error())))
return
}

Expand All @@ -293,7 +300,7 @@ func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Length", fmt.Sprintf("%d", len(fileBytes)))
go h.notifyRequest(time.Since(start), req, status)
res.WriteHeader(status)
res.Write(fileBytes)
maybeWriteResponseBody(fileBytes)
return
}

Expand All @@ -303,7 +310,7 @@ func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "text/html; charset=utf-8")
res.Header().Set("Content-Length", fmt.Sprintf("%d", len(html)))
go h.notifyRequest(time.Since(start), req, http.StatusOK)
res.Write(html)
maybeWriteResponseBody(html)
return
}
}
Expand All @@ -312,7 +319,7 @@ func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "text/plain; charset=utf-8")
go h.notifyRequest(time.Since(start), req, http.StatusNotFound)
res.WriteHeader(http.StatusNotFound)
res.Write([]byte("404 - Not Found"))
maybeWriteResponseBody([]byte("404 - Not Found"))
}

// This exposes an event stream to clients using server-sent events:
Expand Down
48 changes: 32 additions & 16 deletions scripts/js-api-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -3654,9 +3654,9 @@ import "after/alias";
},
}

function fetch(host, port, path, { headers } = {}) {
function fetch(host, port, path, { headers, method = 'GET' } = {}) {
return new Promise((resolve, reject) => {
http.get({ host, port, path, headers }, res => {
http.request({ method, host, port, path, headers }, res => {
const chunks = []
res.on('data', chunk => chunks.push(chunk))
res.on('end', () => {
Expand All @@ -3670,7 +3670,7 @@ function fetch(host, port, path, { headers } = {}) {
resolve(content)
}
})
}).on('error', reject)
}).on('error', reject).end()
})
}

Expand Down Expand Up @@ -4100,9 +4100,6 @@ let serveTests = {
await writeFileAsync(input, `console.log(123)`)

let onRequest;
let singleRequestPromise = new Promise(resolve => {
onRequest = resolve;
});

const context = await esbuild.context({
entryPoints: [input],
Expand All @@ -4113,21 +4110,40 @@ let serveTests = {
try {
const result = await context.serve({
host: '127.0.0.1',
onRequest,
onRequest: args => onRequest(args),
})
assert.strictEqual(result.host, '127.0.0.1');
assert.strictEqual(typeof result.port, 'number');

const buffer = await fetch(result.host, result.port, '/in.js')
assert.strictEqual(buffer.toString(), `console.log(123);\n`);
assert.strictEqual(fs.readFileSync(input, 'utf8'), `console.log(123)`)
// GET /in.js
{
const singleRequestPromise = new Promise(resolve => { onRequest = resolve });
const buffer = await fetch(result.host, result.port, '/in.js')
assert.strictEqual(buffer.toString(), `console.log(123);\n`);
assert.strictEqual(fs.readFileSync(input, 'utf8'), `console.log(123)`)

let singleRequest = await singleRequestPromise;
assert.strictEqual(singleRequest.method, 'GET');
assert.strictEqual(singleRequest.path, '/in.js');
assert.strictEqual(singleRequest.status, 200);
assert.strictEqual(typeof singleRequest.remoteAddress, 'string');
assert.strictEqual(typeof singleRequest.timeInMS, 'number');
}

let singleRequest = await singleRequestPromise;
assert.strictEqual(singleRequest.method, 'GET');
assert.strictEqual(singleRequest.path, '/in.js');
assert.strictEqual(singleRequest.status, 200);
assert.strictEqual(typeof singleRequest.remoteAddress, 'string');
assert.strictEqual(typeof singleRequest.timeInMS, 'number');
// HEAD /in.js
{
const singleRequestPromise = new Promise(resolve => { onRequest = resolve });
const buffer = await fetch(result.host, result.port, '/in.js', { method: 'HEAD' })
assert.strictEqual(buffer.toString(), ``); // HEAD omits the content
assert.strictEqual(fs.readFileSync(input, 'utf8'), `console.log(123)`)

let singleRequest = await singleRequestPromise;
assert.strictEqual(singleRequest.method, 'HEAD');
assert.strictEqual(singleRequest.path, '/in.js');
assert.strictEqual(singleRequest.status, 200);
assert.strictEqual(typeof singleRequest.remoteAddress, 'string');
assert.strictEqual(typeof singleRequest.timeInMS, 'number');
}
} finally {
await context.dispose();
}
Expand Down

0 comments on commit f48391a

Please sign in to comment.