├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── deno.json ├── egg.json ├── example.ts ├── mod.ts └── mod_test.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: v1rtl 2 | liberapay: v1rtl 3 | github: [talentlessguy] 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the master branch 5 | on: 6 | push: 7 | branches: [master] 8 | pull_request: 9 | branches: [master] 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: denoland/setup-deno@v1 17 | with: 18 | deno-version: v1.x 19 | - name: Run tests 20 | run: deno test -A --coverage=coverage 21 | - name: Create coverage report 22 | run: deno coverage ./coverage --lcov > coverage.lcov 23 | - name: Coveralls 24 | uses: coverallsapp/github-action@master 25 | with: 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | path-to-lcov: ./coverage.lcov 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage* -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": false, 5 | "editor.formatOnSave": true, 6 | "[typescript]": { "editor.defaultFormatter": "denoland.vscode-deno" } 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Deno libraries 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # http_compression 4 | 5 | [![nest badge][nest-badge]](https://nest.land/package/compression) 6 | [![GitHub Workflow Status][gh-actions-img]][github-actions] 7 | [![Codecov][cov-badge]][cov] [![][docs-badge]][docs] 8 | [![][code-quality-img]][code-quality] 9 | 10 |
11 | 12 | Deno HTTP compression middleware. 13 | 14 | ## Features 15 | 16 | - `gzip`, `deflate` and `brotli` support 17 | - Detects supported encodings with `Accept-Encoding` header 18 | - Respects encodings order (depending on `Accept-Encoding` value) 19 | - Creates a `Content-Encoding` header with applied compression 20 | - Send `409 Not Acceptable` if encoding is not supported 21 | 22 | ## Example 23 | 24 | ```ts 25 | import { compression } from 'https://deno.land/x/http_compression/mod.ts' 26 | import { Server } from 'https://deno.land/http/server.ts' 27 | 28 | const s = new Server({ 29 | handler: async (req) => { 30 | return await compression({ 31 | path: 'README.md', 32 | compression: ['br', 'gzip', 'deflate'], 33 | })(req) 34 | }, 35 | addr: ':3000', 36 | }) 37 | 38 | s.listenAndServe() 39 | ``` 40 | 41 | Now try to send a `HEAD` request with `curl`: 42 | 43 | ```sh 44 | $ curl localhost:3000 --head -H "Accept-Encoding: br, gzip, deflate" --compressed 45 | HTTP/1.1 200 OK 46 | content-length: 550 47 | content-encoding: br, gzip, deflate 48 | ``` 49 | 50 | [docs-badge]: https://img.shields.io/github/v/release/deno-libs/http_compression?label=Docs&logo=deno&style=for-the-badge&color=black 51 | [docs]: https://doc.deno.land/https/deno.land/x/http_compression/mod.ts 52 | [gh-actions-img]: http://img.shields.io/github/actions/workflow/status/deno-libs/http_compression/main.yml?branch=master&style=for-the-badge&logo=github&label=&color=black 53 | [github-actions]: https://github.com/deno-libs/http_compression/actions 54 | [cov]: https://coveralls.io/github/deno-libs/http_compression 55 | [cov-badge]: https://img.shields.io/coveralls/github/deno-libs/http_compression?style=for-the-badge&color=black 56 | [nest-badge]: https://img.shields.io/badge/publushed%20on-nest.land-black?style=for-the-badge 57 | [code-quality-img]: https://img.shields.io/codefactor/grade/github/deno-libs/http_compression?style=for-the-badge&color=black 58 | [code-quality]: https://www.codefactor.io/repository/github/deno-libs/http_compression 59 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "fmt": { 3 | "options": { 4 | "useTabs": false, 5 | "lineWidth": 80, 6 | "indentWidth": 2, 7 | "singleQuote": true, 8 | "semiColons": false 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /egg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://x.nest.land/eggs@0.3.8/src/schema.json", 3 | "name": "http_compression", 4 | "entry": "./mod.ts", 5 | "description": "🗜️ Deno HTTP compression middleware", 6 | "homepage": "https://deno.land/x/http_compression", 7 | "version": "0.3.1", 8 | "releaseType": "patch", 9 | "unstable": true, 10 | "unlisted": false, 11 | "files": [ 12 | "README.md", 13 | "./**/*.ts" 14 | ], 15 | "ignore": [ 16 | "example.ts" 17 | ], 18 | "checkFormat": false, 19 | "checkTests": true, 20 | "checkInstallation": true, 21 | "check": true, 22 | "checkAll": true, 23 | "repository": "https://deno.land/x/http_compression" 24 | } 25 | -------------------------------------------------------------------------------- /example.ts: -------------------------------------------------------------------------------- 1 | import { compression } from './mod.ts' 2 | import { Server } from 'https://deno.land/std@0.181.0/http/server.ts' 3 | 4 | const s = new Server({ 5 | handler: async (req) => { 6 | return await compression({ 7 | path: 'README.md', 8 | })(req) 9 | }, 10 | port: 3000, 11 | }) 12 | 13 | s.listenAndServe() 14 | console.log( 15 | 'Server available at http://localhost:3000. Set Accept-Encoding header to \'gzip\', for example, to get a compressed response.', 16 | ) 17 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import { readAll } from 'https://deno.land/std@0.181.0/streams/read_all.ts' 2 | import { compress as brotli } from 'https://deno.land/x/brotli@0.1.7/mod.ts' 3 | import { Foras } from 'https://deno.land/x/foras@2.0.8/src/deno/mod.ts' 4 | import { Accepts } from 'https://deno.land/x/accepts@2.1.1/mod.ts' 5 | 6 | await Foras.initBundledOnce() 7 | 8 | const funcs = { 9 | br: brotli, 10 | gzip: (body: Uint8Array) => Foras.gzip(body, undefined), 11 | deflate: (body: Uint8Array) => Foras.deflate(body, undefined), 12 | } 13 | 14 | /** 15 | * Supported compression algorithms 16 | */ 17 | type Compression = 'gzip' | 'br' | 'deflate' 18 | 19 | export type CompressionOptions = Partial<{ 20 | /** 21 | * Path to file 22 | */ 23 | path: string 24 | 25 | /** 26 | * Body as a byte array (as returned from Deno.readFile methods) 27 | */ 28 | bodyBinary: Uint8Array 29 | 30 | /** 31 | * Body as a string (as returned from Deno.readTextFile) 32 | */ 33 | bodyText: string 34 | }> 35 | 36 | /** 37 | * HTTP Compression middleware. 38 | * @param {CompressionOptions} opts 39 | * 40 | * @example 41 | * ```ts 42 | import { compression } from 'https://deno.land/x/http_compression/mod.ts' 43 | import { Server } from 'https://deno.land/std@0.181.0/http/server.ts' 44 | 45 | new Server({ 46 | handler: async (req) => { 47 | return await compression({ path, compression: ['br', 'gzip', 'deflate'] })(req) 48 | }, port: 3000 49 | }).listenAndServe() 50 | * ``` 51 | */ 52 | export const compression = 53 | (opts: CompressionOptions) => async (req: Request): Promise => { 54 | const acceptHeader = req.headers.get('Accept-Encoding') 55 | 56 | const accepts = new Accepts(req.headers) 57 | 58 | const encodings = accepts.encodings() 59 | 60 | let buf: Uint8Array 61 | if (opts.bodyBinary) { 62 | buf = opts.bodyBinary 63 | } else if (opts.bodyText) { 64 | const encoder = new TextEncoder() 65 | buf = encoder.encode(opts.bodyText) 66 | } else if (opts.path) { 67 | const file = await Deno.open(opts.path) 68 | buf = await readAll(file) 69 | file.close() 70 | } else { 71 | throw Error('Must specify either bodyBinary, bodyText, or path.') 72 | } 73 | 74 | if ( 75 | !acceptHeader || acceptHeader === 'identity' || 76 | (Array.isArray(encodings) && encodings[0] === 'identity') 77 | ) { 78 | return new Response(buf, { 79 | status: 200, 80 | headers: new Headers({ 81 | 'Content-Encoding': 'identity', 82 | }), 83 | }) 84 | } else if (acceptHeader === '*') { 85 | const compressed = funcs.gzip(buf) 86 | 87 | return new Response(compressed, { 88 | headers: new Headers({ 89 | 'Content-Encoding': 'gzip', 90 | }), 91 | }) 92 | } else { 93 | if (Array.isArray(encodings)) { 94 | let compressed: Uint8Array = buf 95 | const encs: string[] = [] 96 | 97 | for (let enc of encodings.filter((x) => x !== 'identity')) { 98 | if (enc === 'brotli') enc = 'br' 99 | 100 | if (Object.keys(funcs).includes(enc as string)) { 101 | compressed = funcs[enc as Compression](compressed) 102 | encs.push(enc) 103 | } 104 | } 105 | 106 | return new Response(compressed, { 107 | headers: new Headers({ 108 | 'Content-Encoding': encs.join(', '), 109 | }), 110 | }) 111 | } else { 112 | return Object.keys(funcs).includes(encodings as string) 113 | ? new Response(funcs[encodings as Compression](buf), { 114 | headers: new Headers({ 115 | 'Content-Encoding': encodings as string, 116 | }), 117 | }) 118 | : new Response('Not Acceptable', { 119 | status: 406, 120 | }) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /mod_test.ts: -------------------------------------------------------------------------------- 1 | import { superdeno } from 'https://deno.land/x/superdeno@4.8.0/mod.ts' 2 | import { describe, it, run } from 'https://deno.land/x/tincan@1.0.1/mod.ts' 3 | import { compression } from './mod.ts' 4 | 5 | describe('options', () => { 6 | it('does not apply compression if "Accept-Encoding" is "identity"', async () => { 7 | const request = superdeno(compression({ path: 'LICENSE' })) 8 | 9 | await request 10 | .get('/') 11 | .set('Accept-Encoding', 'identity') 12 | .expect(200) 13 | .expect('Content-Encoding', 'identity') 14 | .expect('Content-Length', '1071') 15 | }) 16 | it('applies compression to a file', async () => { 17 | const request = superdeno( 18 | compression({ 19 | path: 'LICENSE', 20 | }), 21 | ) 22 | await request 23 | .get('/') 24 | .set('Accept-Encoding', 'br, gzip, deflate') 25 | .expect('Content-Length', '628') 26 | .expect('Content-Encoding', 'br, gzip, deflate') 27 | }) 28 | it('applies compression to a string', async () => { 29 | const bodyText = await Deno.readTextFile('LICENSE') 30 | const request = superdeno(compression({ bodyText })) 31 | 32 | await request 33 | .get('/') 34 | .set('Accept-Encoding', 'br, gzip, deflate') 35 | .expect('Content-Length', '628') 36 | .expect('Content-Encoding', 'br, gzip, deflate') 37 | }) 38 | it('applies compression to a byte array', async () => { 39 | const bodyBinary = await Deno.readFile('LICENSE') 40 | const request = superdeno(compression({ bodyBinary })) 41 | 42 | await request 43 | .get('/') 44 | .set('Accept-Encoding', 'br, gzip, deflate') 45 | .expect('Content-Length', '628') 46 | .expect('Content-Encoding', 'br, gzip, deflate') 47 | }) 48 | }) 49 | 50 | run() 51 | --------------------------------------------------------------------------------