chai-http: HTTP assertions for Chai, with a superagent chainable API
HTTP Response assertions for the Chai Assertion Library.
At a glance
- What is it?
- chai-http is a Chai plugin that adds HTTP response assertions to your test suite, wrapping superagent in a chainable request builder. It lets you test an Express app in process or a running service over HTTP, and it comes with one sharp edge: the callback form of end() can let a test pass before its assertions run.
- Who is it for?
- Adopt chai-http when your tests need to assert on real HTTP responses with Chai syntax, either against an Express app started in process or against a service on a URL. Do not adopt it for browser based testing of a page, because the documentation points browser users to the v4 release, and do not use the callback form of end() without signalling completion to your test runner.
- Can I use it commercially?
- Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the plugin actually adds to Chai
chai-http is an addon plugin for the Chai Assertion Library, and its purpose is narrow: HTTP response assertions. The README lists the feature set as integration test request composition, testing http apps or external services, assertions for common http tasks, and the chai expect and should interfaces.
That last item matters more than it sounds. The plugin does not bring a new assertion vocabulary. It extends the assertion library you already use, so expect(res).to.have.status(200) reads the same way your other assertions do, and the should interface works as well.
Underneath, the integration testing interface is provided via superagent. So the plugin is best understood as a bridge: it takes superagent's request builder and gives it a Chai-aware shape and an assertion surface, plus the ability to start and stop a server for you.
The project description in package.json is blunter: extend Chai Assertion library with tests for http apis. The package keywords list supertest and superagent alongside chai and chai-plugin, which signals the lineage it belongs to.
For teams that already write unit tests with Chai and now need to cover an HTTP boundary, the appeal is that there is one assertion style across the whole suite rather than a second framework for HTTP alone.
Installing the plugin and wiring it into Chai
Installation is a single npm install:
npm install chai-httpRegistering the plugin follows the usual Chai pattern. The plugin is used as any other Chai plugin:
import * as chai from "chai";
import chaiHttp from "chai-http";
chai.use(chaiHttp);If you need direct access to the request object, the plugin can be imported differently, exposing both the default plugin and the request helper:
// if you need to access `request`
import {default as chaiHttp, request} from "chai-http";
chai.use(chaiHttp);
request.get(...).send(...);
// or setting up an app
request.execute(app);Two things about the package metadata are worth knowing before you upgrade anything. The version field in package.json reads 0.0.0-development, which is the semantic-release convention for a version that is filled in at publish time, so the version you see in your lockfile comes from the registry rather than from the source file.
The licence field says MIT, while the repository licence is listed as something GitHub cannot classify. The package.json is the more specific statement about the published artifact, but if your organisation needs the exact terms, the LICENSE file is the place to read.
The dependencies list is short and tells you the shape of the implementation: superagent at 10.0.0 or later does the HTTP work, cookiejar at 2.1.4 or later handles cookie storage, qs at 6.12.1 or later builds query strings, methods and charset supply HTTP verb and encoding data, and is-ip helps classify addresses.
Two ways to point a request at something: an app or a URL
The request foundation is either an application object or a base URL, and the plugin handles the lifecycle difference for you.
With an application, you pass a function such as an Express or connect app, or a node.js http(s) server. The documentation notes that if the server is not running, chai-http finds a suitable port to listen on for a given test:
import {request} from 'chai-http';
request.execute(app)
.get('/')The behaviour worth internalising is that passing an app to request.execute() opens the server by calling listen() and, once the request has been made, shuts it down by calling .close(). That is convenient for a single request, but it means a second request in the same test needs a running server.
The fix is .keepOpen() on the requester, which keeps the server up so several requests can run, with a manual close at the end:
import {request} from 'chai-http';
const requester = request.Request(app).keepOpen()
Promise.all([
requester.get('/a'),
requester.get('/b'),
])
.then(responses => { /* ... */ })
.then(() => requester.close())With a URL, the base is passed instead, and the server lifecycle is entirely yours:
import {request} from 'chai-http';
request.execute('http://localhost:8080')
.get('/')One environment restriction is stated plainly. The application and server feature is only supported on Node.js, not in web browsers. That constraint shows up later in the packaging, where the browser field in package.json maps the http, https and net modules to browser-safe replacements.
The request builder chain, method by method
Once a request exists with a verb, you chain on the methods that set it up. The methods are listed in a table in the documentation: .set(key, value) for request headers, .send(data) for request data with JSON as the default type, .type(dataType) to change the type of data sent by .send(), .attach(field, file, attachment) to attach a file, .auth(username, password) to add Basic Authentication headers, and .query(parmasObject) to chain GET parameters.
The .type() method deserves a note because the default catches people out. Since .send() sends JSON unless told otherwise, a form encoded POST needs .type('form') explicitly:
import {request} from 'chai-http';
// Send some Form Data
request.execute(app)
.post('/user/me')
.type('form')
.send({
'_method': 'put',
'password': '123',
'confirmPassword': '123'
})Authentication supports both schemes. Basic auth takes a username and password, while a bearer token is passed as a single value with a type option:
import {request} from 'chai-http';
// Authenticate with Basic authentication
request.execute(app)
.get('/protected')
.auth('user', 'pass')
// Authenticate with Bearer Token
request.execute(app)
.get('/protected')
.auth(accessToken, { type: 'bearer' })Files attach through .attach(), which takes a field name, the file contents and a filename:
import {request} from 'chai-http';
// Attach a file
request.execute(app)
.post('/user/avatar')
.attach('imageField', fs.readFileSync('avatar.png'), 'avatar.png')And query parameters are chained in one call, with the resulting URL shown in the comment:
import {request} from 'chai-http';
// Chain some GET query parameters
request.execute(app)
.get('/search')
.query({name: 'foo', limit: 10}) // /search?name=foo&limit=10The chain reads like superagent because it is superagent, with the plugin's assertion layer attached. There is no separate configuration object to learn.
The end() callback trap that silently passes a failing test
There are two ways to consume a response, and the callback form carries a documented trap.
The callback form uses the end method:
import {request} from 'chai-http';
request.execute(app)
.put('/user/me')
.send({ password: '123', confirmPassword: '123' })
.end((err, res) => {
expect(err).to.be.null;
expect(res).to.have.status(200);
});Because end takes a callback, the assertions run asynchronously. The documentation is explicit that a mechanism must be used to notify the testing framework that the callback has completed, otherwise the test passes before the assertions are checked. That means a test asserting a deliberately wrong status can go green.
The Mocha example shows both failure shapes side by side:
import {request} from 'chai-http';
it('fails, as expected', function(done) { // <= Pass in done callback
request.execute('http://localhost:8080')
.get('/')
.end((err, res) => {
expect(res).to.have.status(123);
done(); // <= Call done to signal callback end
});
});
it('succeeds silently!', () => { // <= No done callback
request.execute('http://localhost:8080')
.get('/')
.end((err, res) => {
expect(res).to.have.status(123); // <= Test completes before this runs
});
});The second test asserts a status of 123 and would still pass, because the runner has already finished the test by the time the assertion executes.
The promise form avoids the problem, because the chain itself does not complete until the handler runs:
import {request} from 'chai-http';
request.execute(app)
.put('/user/me')
.send({ password: '123', confirmPassword: '123' })
.then((res) => {
expect(res).to.have.status(200);
})
.catch((err) => {
throw err;
});The choice between the two is not stylistic. If you use the callback form in Mocha, the done callback has to be called, and it accepts an error parameter when signalling completion. If you use the promise form in an async test, a thrown assertion inside then is caught by the runner properly.
Cookies across requests, and the browser story
Some tests need state carried between calls, typically logging in with one request and then hitting an authenticated resource with the next. The plugin exposes .request.agent() for that, and the cookiejar dependency is what makes it work.
The browser case is where the documentation draws a hard line. To use Chai HTTP on a web page, the README says to use the latest v4 version for now. That is a specific version instruction for browser consumers and it stands against the current 5.x line.
The packaging explains why the browser story is awkward. The browser field in package.json replaces the http and https modules with false and substitutes a local lib/net.js for net, with qs standing in for querystring. That is the standard technique for making a Node oriented library load in a bundler, and it works only as far as the plugin's own features allow.
The server-and-port feature does not survive that substitution, since the documentation states it is supported only on Node.js and not in web browsers. In other words, browser users get a request builder pointed at URLs, not the in-process application lifecycle management.
The type definitions ship separately. The files list includes types/index.d.ts and the exports map points the types condition at it, with a build script that compiles them by changing into the types directory and running tsc. So TypeScript users get types for the plugin without a separate type package.
Releases, tooling and what is still in flight
The tagged releases are 5.1.2 on 2025-05-02, 5.1.1 on 2024-10-06 and 5.1.0 on 2024-09-02. The last push to the repository was on 2026-10-19, so the tree is receiving changes well after the newest tag, which is what the semantic-release badge in the README heading suggests.
The build and test tooling is conventional. The test script runs c8 with a coverage reporter alongside mocha, and a separate coverage script pushes to coveralls when a COVERALLS_REPO_TOKEN environment variable is set. The linting is eslint with a flat config file and the mocha plugin. The development workflow starts a watcher and an http-server in parallel, with the server flag opening a browser and disabling cache.
Because the plugin is versioned on its own line while Chai itself moved to a major version, the peer relationship is the thing to check when you upgrade. The devDependencies list chai at 6.0.0 or later, and the assertions shown in the README use the destructured form, const { expect } = chai, which suits that generation of Chai.
For a team, the practical picture is a small, focused plugin whose API is stable in practice because it mirrors superagent, but whose browser support trails the Node support. Read the History.md file in the repository when upgrading between minor versions, since that is where the project records what changed.
Editorial conclusion
Adopt chai-http when your tests need to assert on real HTTP responses with Chai syntax, either against an Express app started in process or against a service on a URL. Do not adopt it for browser based testing of a page, because the documentation points browser users to the v4 release, and do not use the callback form of end() without signalling completion to your test runner. Before upgrading, check the version you are on, since the last tagged release was 5.1.2 on 2025-05-02 while the working tree has continued to move, and read the browser section of package.json if your suite runs in a browser at all.
Frequently asked questions
How do I install and register the chai-http plugin?
Install it with npm install chai-http, then register it with chai.use(chaiHttp). If you also need the request helper directly, import it as {default as chaiHttp, request} from "chai-http".
How do I test an Express app with chai-http without starting the server myself?
Pass the app to request.execute(app). The plugin calls listen() to open the server and closes it once the request has been made. If you need several requests against the same app, use request.Request(app).keepOpen() and close the requester yourself afterwards.
Why does my chai-http test pass even though the assertion should fail?
The end() method takes a callback, so assertions run asynchronously and the test can complete before they are checked. The documentation shows that you must signal completion, for example with the done callback in Mocha, or use the promise form of the request chain instead.
Can I use chai-http in a web browser?
The README says to use the latest v4 version for browser use for now, and the application and server feature is supported only on Node.js. The browser field in package.json substitutes the http, https and net modules with browser-safe replacements.
How do I send form data instead of JSON with chai-http?
Call .type('form') before .send(), since .send() defaults to JSON. The README documents .type(dataType) for changing the type of the data sent by .send().
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/chaijs-chai-http)Was this useful?