Skip to content

Commit

Permalink
Simplify middleware approach using Koa-like fns
Browse files Browse the repository at this point in the history
Closes #48
  • Loading branch information
blakeembrey committed Apr 14, 2016
1 parent 8941dcd commit f00ef6e
Show file tree
Hide file tree
Showing 13 changed files with 370 additions and 496 deletions.
23 changes: 9 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Build status][travis-image]][travis-url]
[![Test coverage][coveralls-image]][coveralls-url]

**Popsicle** is the easiest way to make HTTP requests - offering a consistent, intuitive and light-weight API that works on node and the browser.
> **Popsicle** is the easiest way to make HTTP requests - offering a consistent, intuitive and light-weight API that works on node and the browser.
```js
popsicle.get('/users.json')
Expand Down Expand Up @@ -254,7 +254,7 @@ popsicle.get('/users')
})
```

If you live on the edge, try using it with generators (see [co](https://www.npmjs.com/package/co)) or ES7's `async`.
If you live on the edge, try using it with generators (see [co](https://www.npmjs.com/package/co)) or ES7 `async`/`await`.

```js
co(function * () {
Expand Down Expand Up @@ -330,12 +330,13 @@ Plugins can be passed in as an array with the initial options (which overrides d

#### Creating Plugins

Plugins must be a function that accepts configuration and returns another function. For example, here's a basic URL prefix plugin.
Plugins must be a function that accept config and return a middleware function. For example, here's a basic URL prefix plugin.

```js
function prefix (url) {
return function (self) {
request.url = url + req.url
return function (self, next) {
self.url = url + self.url
return next()
}
}

Expand All @@ -346,13 +347,7 @@ popsicle.request('/user')
})
```

Popsicle also has a way modify the request and response lifecycle, if needed. Any registered function can return a promise to defer the request or response resolution. This makes plugins such as rate-limiting and response body concatenation possible.

* **before(fn)** Register a function to run before the request is made
* **after(fn)** Register a function to receive the response object
* **always(fn)** Register a function that always runs on `resolve` or `reject`

**Tip:** Use the lifecycle hooks (above) when you want re-use (E.g. re-use when the request is cloned or options re-used).
Middleware functions accept two arguments - the current request and a function to proceed to the next middleware function (a la Koa `2.x`).

#### Checking The Environment

Expand All @@ -362,11 +357,11 @@ popsicle.browser //=> true

#### Transportation Layers

Creating a custom transportation layer is just a matter creating an object with `open`, `abort` and `use` options set. The open method should set any request information required between called as `request.raw`. Abort must abort the current request instance, while `open` must **always** resolve the promise. You can set `use` to an empty array if no plugins should be used by default. However, it's recommended you keep `use` set to the defaults, or as close as possible using your transport layer.
Creating a custom transportation layer is just a matter creating an object with `open`, `abort` and `use` options set. The open method should set any request information required between called as `request._raw`. Abort must abort the current request instance, while `open` must **always** resolve to a promise. You can set `use` to an empty array if no plugins should be used by default. However, it's recommended you keep `use` set to the defaults, or as close as possible using your transport layer.

## TypeScript

This project is written using [TypeScript](https://github.com/Microsoft/TypeScript) and [typings](https://github.com/typings/typings). From version `1.3.1`, you can install the type definition using `typings`.
This project is written using [TypeScript](https://github.com/Microsoft/TypeScript) and [typings](https://github.com/typings/typings). Since version `1.3.1`, you can install the type definition using `typings`.

```
typings install npm:popsicle --save
Expand Down
4 changes: 2 additions & 2 deletions lib/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function open (request: Request) {
return reject(request.error(`The request to "${url}" was blocked`, 'EBLOCKED'))
}

const xhr = request.raw = new XMLHttpRequest()
const xhr = request._raw = new XMLHttpRequest()

xhr.onload = function () {
return resolve({
Expand Down Expand Up @@ -96,7 +96,7 @@ function open (request: Request) {
* Close the current HTTP request.
*/
function abort (request: Request) {
request.raw.abort()
request._raw.abort()
}

/**
Expand Down
96 changes: 56 additions & 40 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function open (request: Request) {
const maxRedirects = num(options.maxRedirects, 5)
const followRedirects = options.followRedirects !== false
let requestCount = 0
let isStreaming = false

const confirmRedirect = typeof options.followRedirects === 'function' ?
options.followRedirects : falsey
Expand All @@ -67,7 +68,7 @@ function open (request: Request) {
const isHttp = arg.protocol !== 'https:'
const engine: typeof httpRequest = isHttp ? httpRequest : httpsRequest

// Always attach certain options.
// Attach request options.
arg.method = method
arg.headers = request.toHeaders()
arg.agent = options.agent
Expand All @@ -76,39 +77,39 @@ function open (request: Request) {
arg.cert = options.cert
arg.key = options.key

const req = engine(arg)
const rawRequest = engine(arg)

// Track upload progress through a stream.
const requestProxy = new PassThrough()
const responseProxy = new PassThrough()
// Track upload/download progress through a stream.
const requestStream = new PassThrough()
const responseStream = new PassThrough()

requestProxy.on('data', function (chunk: Buffer) {
request.uploadedBytes = request.uploadedBytes + chunk.length
requestStream.on('data', function (chunk: Buffer) {
request.uploadedBytes += chunk.length
})

requestProxy.on('end', function () {
requestStream.on('end', function () {
request.uploadedBytes = request.uploadLength
})

responseProxy.on('data', function (chunk: Buffer) {
request.downloadedBytes = request.downloadedBytes + chunk.length
responseStream.on('data', function (chunk: Buffer) {
request.downloadedBytes += chunk.length
})

responseProxy.on('end', function () {
responseStream.on('end', function () {
request.downloadedBytes = request.downloadLength
})

// Handle the HTTP response.
function response (res: IncomingMessage) {
const status = res.statusCode
function response (rawResponse: IncomingMessage) {
const status = rawResponse.statusCode
const redirect = REDIRECT_STATUS[status]

// Handle HTTP redirects.
if (followRedirects && redirect != null && res.headers.location) {
const newUrl = urlLib.resolve(url, res.headers.location)
if (followRedirects && redirect != null && rawResponse.headers.location) {
const newUrl = urlLib.resolve(url, rawResponse.headers.location)

// Ignore the result of the response on redirect.
res.resume()
rawResponse.resume()

// Kill the old cookies on redirect.
request.remove('Cookie')
Expand All @@ -127,57 +128,71 @@ function open (request: Request) {
}

// Allow the user to confirm redirect according to HTTP spec.
if (confirmRedirect(req, res)) {
if (confirmRedirect(rawRequest, rawResponse)) {
return get(newUrl, method, body)
}
}
}

request.downloadLength = num(res.headers['content-length'], 0)
// When the request is streaming already, errors need to be handled differently.
isStreaming = true

request.downloadLength = num(rawResponse.headers['content-length'], 0)

// Track download progress.
res.pipe(responseProxy)
rawResponse.pipe(responseStream)

return Promise.resolve({
body: responseProxy,
body: responseStream,
status: status,
statusText: res.statusMessage,
headers: res.headers,
rawHeaders: res.rawHeaders,
statusText: rawResponse.statusMessage,
headers: rawResponse.headers,
rawHeaders: rawResponse.rawHeaders,
url: url
})
}

// Handle the response.
req.once('response', function (message: IncomingMessage) {
return resolve(setCookies(request, message).then(() => response(message)))
// Emit a request error.
function emitError (error: Error) {
// Abort on error.
rawRequest.abort()

// Forward errors.
if (isStreaming) {
responseStream.emit('error', error)
} else {
reject(error)
}
}

rawRequest.once('response', function (message: IncomingMessage) {
resolve(setCookies(request, message).then(() => response(message)))
})

// io.js has an abort event instead of "error".
req.once('abort', function () {
return reject(request.error('Request aborted', 'EABORT'))
rawRequest.once('error', function (error: Error) {
emitError(request.error(`Unable to connect to "${url}"`, 'EUNAVAILABLE', error))
})

req.once('error', function (error: Error) {
return reject(request.error(`Unable to connect to "${url}"`, 'EUNAVAILABLE', error))
rawRequest.once('clientAborted', function () {
emitError(request.error('Request aborted', 'EABORT'))
})

// Node 0.10 needs to catch errors on the request proxy.
requestProxy.once('error', reject)
requestStream.once('error', reject)

request.raw = req
request.uploadLength = num(req.getHeader('content-length'), 0)
requestProxy.pipe(req)
request._raw = rawRequest
request.uploadLength = num(rawRequest.getHeader('content-length'), 0)
requestStream.pipe(rawRequest)

// Pipe the body to the stream.
if (body) {
if (typeof body.pipe === 'function') {
body.pipe(requestProxy)
body.pipe(requestStream)
body.once('error', emitError)
} else {
requestProxy.end(body)
requestStream.end(body)
}
} else {
requestProxy.end()
requestStream.end()
}
})
})
Expand All @@ -190,7 +205,8 @@ function open (request: Request) {
* Close the current HTTP request.
*/
function abort (request: Request) {
request.raw.abort()
request._raw.emit('clientAborted')
request._raw.abort()
}

/**
Expand Down
Loading

0 comments on commit f00ef6e

Please sign in to comment.