Add API config to allow disabling response size warning (#34700)
Adds an API config option that disables warning a user when their API response body is over 4 megs. This has been added for users who'd like to stream larger amounts of data from their API acknowledging the drawbacks. This config mirrors the existing [`externalResolver` config](https://nextjs.org/docs/api-routes/api-middlewares#custom-config). Closes: [#33162](https://github.com/vercel/next.js/issues/33162) Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
This commit is contained in:
parent
ce4923c659
commit
079b507327
20 changed files with 159 additions and 27 deletions
|
@ -68,6 +68,29 @@ export const config = {
|
|||
}
|
||||
```
|
||||
|
||||
`responseLimit` is automatically enabled, warning when an API routes' response body is over 4MB.
|
||||
|
||||
If you are not using Next.js in a serverless environment, and understand the performance implications of not using a CDN or dedicated media host, you can set this limit to `false`.
|
||||
|
||||
```js
|
||||
export const config = {
|
||||
api: {
|
||||
responseLimit: false,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`responseLimit` can also take the number of bytes or any string format supported by `bytes`, for example `1000`, `'500kb'` or `'3mb'`.
|
||||
This value will be the maximum response size before a warning is displayed. Default is 4MB. (see above)
|
||||
|
||||
```js
|
||||
export const config = {
|
||||
api: {
|
||||
responseLimit: '8mb',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Connect/Express middleware support
|
||||
|
||||
You can also use [Connect](https://github.com/senchalabs/connect) compatible middleware.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# API Routes Body Size Limited to 4MB
|
||||
# API Routes Response Size Limited to 4MB
|
||||
|
||||
#### Why This Error Occurred
|
||||
|
|
@ -42,7 +42,11 @@
|
|||
},
|
||||
{
|
||||
"title": "api-routes-body-size-limit",
|
||||
"path": "/errors/api-routes-body-size-limit.md"
|
||||
"path": "/errors/api-routes-response-size-limit.md"
|
||||
},
|
||||
{
|
||||
"title": "api-routes-response-size-limit",
|
||||
"path": "/errors/api-routes-response-size-limit.md"
|
||||
},
|
||||
{
|
||||
"title": "api-routes-static-export",
|
||||
|
|
23
packages/next/compiled/bytes/LICENSE
Normal file
23
packages/next/compiled/bytes/LICENSE
Normal file
|
@ -0,0 +1,23 @@
|
|||
(The MIT License)
|
||||
|
||||
Copyright (c) 2012-2014 TJ Holowaychuk <tj@vision-media.ca>
|
||||
Copyright (c) 2015 Jed Watson <jed.watson@me.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
8
packages/next/compiled/bytes/index.js
Normal file
8
packages/next/compiled/bytes/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
(()=>{"use strict";var e={446:e=>{
|
||||
/*!
|
||||
* bytes
|
||||
* Copyright(c) 2012-2014 TJ Holowaychuk
|
||||
* Copyright(c) 2015 Jed Watson
|
||||
* MIT Licensed
|
||||
*/
|
||||
e.exports=bytes;e.exports.format=format;e.exports.parse=parse;var r=/\B(?=(\d{3})+(?!\d))/g;var a=/(?:\.0*|(\.[^0]+)0+)$/;var t={b:1,kb:1<<10,mb:1<<20,gb:1<<30,tb:Math.pow(1024,4),pb:Math.pow(1024,5)};var i=/^((-|\+)?(\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i;function bytes(e,r){if(typeof e==="string"){return parse(e)}if(typeof e==="number"){return format(e,r)}return null}function format(e,i){if(!Number.isFinite(e)){return null}var n=Math.abs(e);var o=i&&i.thousandsSeparator||"";var s=i&&i.unitSeparator||"";var f=i&&i.decimalPlaces!==undefined?i.decimalPlaces:2;var u=Boolean(i&&i.fixedDecimals);var p=i&&i.unit||"";if(!p||!t[p.toLowerCase()]){if(n>=t.pb){p="PB"}else if(n>=t.tb){p="TB"}else if(n>=t.gb){p="GB"}else if(n>=t.mb){p="MB"}else if(n>=t.kb){p="KB"}else{p="B"}}var b=e/t[p.toLowerCase()];var l=b.toFixed(f);if(!u){l=l.replace(a,"$1")}if(o){l=l.split(".").map((function(e,a){return a===0?e.replace(r,o):e})).join(".")}return l+s+p}function parse(e){if(typeof e==="number"&&!isNaN(e)){return e}if(typeof e!=="string"){return null}var r=i.exec(e);var a;var n="b";if(!r){a=parseInt(e,10);n="b"}else{a=parseFloat(r[1]);n=r[4].toLowerCase()}return Math.floor(t[n]*a)}}};var r={};function __nccwpck_require__(a){var t=r[a];if(t!==undefined){return t.exports}var i=r[a]={exports:{}};var n=true;try{e[a](i,i.exports,__nccwpck_require__);n=false}finally{if(n)delete r[a]}return i.exports}if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=__dirname+"/";var a=__nccwpck_require__(446);module.exports=a})();
|
1
packages/next/compiled/bytes/package.json
Normal file
1
packages/next/compiled/bytes/package.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"name":"bytes","main":"index.js","author":"TJ Holowaychuk <tj@vision-media.ca> (http://tjholowaychuk.com)","license":"MIT"}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -132,6 +132,7 @@
|
|||
"@types/babel__generator": "7.6.2",
|
||||
"@types/babel__template": "7.4.0",
|
||||
"@types/babel__traverse": "7.11.0",
|
||||
"@types/bytes": "3.1.1",
|
||||
"@types/ci-info": "2.0.0",
|
||||
"@types/compression": "0.0.36",
|
||||
"@types/content-disposition": "0.5.4",
|
||||
|
@ -168,6 +169,7 @@
|
|||
"async-sema": "3.0.0",
|
||||
"babel-plugin-transform-define": "2.0.0",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"bytes": "3.1.1",
|
||||
"browserify-zlib": "0.2.0",
|
||||
"browserslist": "4.18.1",
|
||||
"buffer": "5.6.0",
|
||||
|
|
|
@ -82,6 +82,8 @@ export function checkIsManualRevalidate(
|
|||
export const COOKIE_NAME_PRERENDER_BYPASS = `__prerender_bypass`
|
||||
export const COOKIE_NAME_PRERENDER_DATA = `__next_preview_data`
|
||||
|
||||
export const RESPONSE_LIMIT_DEFAULT = 4 * 1024 * 1024
|
||||
|
||||
export const SYMBOL_PREVIEW_DATA = Symbol(COOKIE_NAME_PRERENDER_DATA)
|
||||
export const SYMBOL_CLEARED_COOKIES = Symbol(COOKIE_NAME_PRERENDER_BYPASS)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import type { BaseNextRequest, BaseNextResponse } from '../base-http'
|
|||
import type { CookieSerializeOptions } from 'next/dist/compiled/cookie'
|
||||
import type { PreviewData } from 'next/types'
|
||||
|
||||
import bytes from 'next/dist/compiled/bytes'
|
||||
import jsonwebtoken from 'next/dist/compiled/jsonwebtoken'
|
||||
import { decryptWithSecret, encryptWithSecret } from '../crypto-utils'
|
||||
import generateETag from 'next/dist/compiled/etag'
|
||||
|
@ -28,6 +29,7 @@ import {
|
|||
COOKIE_NAME_PRERENDER_BYPASS,
|
||||
COOKIE_NAME_PRERENDER_DATA,
|
||||
SYMBOL_PREVIEW_DATA,
|
||||
RESPONSE_LIMIT_DEFAULT,
|
||||
} from './index'
|
||||
|
||||
export function tryGetPreviewData(
|
||||
|
@ -172,6 +174,7 @@ export async function apiResolver(
|
|||
}
|
||||
const config: PageConfig = resolverModule.config || {}
|
||||
const bodyParser = config.api?.bodyParser !== false
|
||||
const responseLimit = config.api?.responseLimit ?? true
|
||||
const externalResolver = config.api?.externalResolver || false
|
||||
|
||||
// Parsing of cookies
|
||||
|
@ -198,6 +201,7 @@ export async function apiResolver(
|
|||
}
|
||||
|
||||
let contentLength = 0
|
||||
const maxContentLength = getMaxContentLength(responseLimit)
|
||||
const writeData = apiRes.write
|
||||
const endResponse = apiRes.end
|
||||
apiRes.write = (...args: any[2]) => {
|
||||
|
@ -209,9 +213,11 @@ export async function apiResolver(
|
|||
contentLength += Buffer.byteLength(args[0] || '')
|
||||
}
|
||||
|
||||
if (contentLength >= 4 * 1024 * 1024) {
|
||||
if (responseLimit && contentLength >= maxContentLength) {
|
||||
console.warn(
|
||||
`API response for ${req.url} exceeds 4MB. This will cause the request to fail in a future version. https://nextjs.org/docs/messages/api-routes-body-size-limit`
|
||||
`API response for ${req.url} exceeds ${bytes.format(
|
||||
maxContentLength
|
||||
)}. API Routes are meant to respond quickly. https://nextjs.org/docs/messages/api-routes-response-size-limit`
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -484,3 +490,10 @@ function setPreviewData<T>(
|
|||
])
|
||||
return res
|
||||
}
|
||||
|
||||
function getMaxContentLength(responseLimit?: number | string | boolean) {
|
||||
if (responseLimit && typeof responseLimit !== 'boolean') {
|
||||
return bytes.parse(responseLimit)
|
||||
}
|
||||
return RESPONSE_LIMIT_DEFAULT
|
||||
}
|
||||
|
|
|
@ -830,6 +830,14 @@ export async function ncc_babel_bundle_packages(task, opts) {
|
|||
.target('compiled/babel')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
externals['bytes'] = 'next/dist/compiled/bytes'
|
||||
export async function ncc_bytes(task, opts) {
|
||||
await task
|
||||
.source(opts.src || relative(__dirname, require.resolve('bytes')))
|
||||
.ncc({ packageName: 'bytes', externals })
|
||||
.target('compiled/bytes')
|
||||
}
|
||||
// eslint-disable-next-line camelcase
|
||||
externals['ci-info'] = 'next/dist/compiled/ci-info'
|
||||
export async function ncc_ci_info(task, opts) {
|
||||
|
@ -1628,6 +1636,7 @@ export async function ncc(task, opts) {
|
|||
'ncc_tty_browserify',
|
||||
'ncc_vm_browserify',
|
||||
'ncc_babel_bundle',
|
||||
'ncc_bytes',
|
||||
'ncc_ci_info',
|
||||
'ncc_cli_select',
|
||||
'ncc_comment_json',
|
||||
|
|
6
packages/next/types/index.d.ts
vendored
6
packages/next/types/index.d.ts
vendored
|
@ -63,6 +63,12 @@ export type NextPage<P = {}, IP = P> = NextComponentType<NextPageContext, IP, P>
|
|||
export type PageConfig = {
|
||||
amp?: boolean | 'hybrid'
|
||||
api?: {
|
||||
/**
|
||||
* Configures or disables body size limit warning. Can take a number or
|
||||
* any string format supported by `bytes`, for example `1000`, `'500kb'` or
|
||||
* `'3mb'`.
|
||||
*/
|
||||
responseLimit?: number | string | boolean
|
||||
/**
|
||||
* The byte limit of the body. This is the number of bytes or any string
|
||||
* format supported by `bytes`, for example `1000`, `'500kb'` or `'3mb'`.
|
||||
|
|
4
packages/next/types/misc.d.ts
vendored
4
packages/next/types/misc.d.ts
vendored
|
@ -143,6 +143,10 @@ declare module 'next/dist/compiled/babel/core-lib-normalize-opts'
|
|||
declare module 'next/dist/compiled/babel/core-lib-block-hoist-plugin'
|
||||
declare module 'next/dist/compiled/babel/core-lib-plugin-pass'
|
||||
|
||||
declare module 'next/dist/compiled/bytes' {
|
||||
import m from 'bytes'
|
||||
export = m
|
||||
}
|
||||
declare module 'next/dist/compiled/ci-info' {
|
||||
import m from 'ci-info'
|
||||
export = m
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export default (req, res) => {
|
||||
for (let i = 0; i <= 5 * 1024 * 1024; i++) {
|
||||
for (let i = 0; i <= 4 * 1024 * 1024; i++) {
|
||||
res.write('.')
|
||||
}
|
||||
res.end()
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
export const config = {
|
||||
api: {
|
||||
responseLimit: '5mb',
|
||||
},
|
||||
}
|
||||
|
||||
export default (req, res) => {
|
||||
let body = '.'.repeat(6 * 1024 * 1024)
|
||||
res.send(body)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
export const config = {
|
||||
api: {
|
||||
responseLimit: false,
|
||||
},
|
||||
}
|
||||
|
||||
export default (req, res) => {
|
||||
let body = '.'.repeat(4 * 1024 * 1024)
|
||||
res.send(body)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export default (req, res) => {
|
||||
let body = '.'.repeat(5 * 1024 * 1024)
|
||||
let body = '.'.repeat(4 * 1024 * 1024)
|
||||
res.send(body)
|
||||
}
|
||||
|
|
|
@ -451,13 +451,32 @@ function runTests(dev = false) {
|
|||
let res = await fetchViaHTTP(appPort, '/api/large-response')
|
||||
expect(res.ok).toBeTruthy()
|
||||
expect(stderr).toContain(
|
||||
'API response for /api/large-response exceeds 4MB. This will cause the request to fail in a future version.'
|
||||
'API response for /api/large-response exceeds 4MB. API Routes are meant to respond quickly.'
|
||||
)
|
||||
|
||||
res = await fetchViaHTTP(appPort, '/api/large-chunked-response')
|
||||
expect(res.ok).toBeTruthy()
|
||||
expect(stderr).toContain(
|
||||
'API response for /api/large-chunked-response exceeds 4MB. This will cause the request to fail in a future version.'
|
||||
'API response for /api/large-chunked-response exceeds 4MB. API Routes are meant to respond quickly.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not warn if response body is larger than 4MB with responseLimit config = false', async () => {
|
||||
let res = await fetchViaHTTP(appPort, '/api/large-response-with-config')
|
||||
expect(res.ok).toBeTruthy()
|
||||
expect(stderr).not.toContain(
|
||||
'API response for /api/large-response-with-config exceeds 4MB. API Routes are meant to respond quickly.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should warn with configured size if response body is larger than configured size', async () => {
|
||||
let res = await fetchViaHTTP(
|
||||
appPort,
|
||||
'/api/large-response-with-config-size'
|
||||
)
|
||||
expect(res.ok).toBeTruthy()
|
||||
expect(stderr).toContain(
|
||||
'API response for /api/large-response-with-config-size exceeds 5MB. API Routes are meant to respond quickly.'
|
||||
)
|
||||
})
|
||||
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -4796,6 +4796,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/braces/-/braces-3.0.1.tgz#5a284d193cfc61abb2e5a50d36ebbc50d942a32b"
|
||||
integrity sha512-+euflG6ygo4bn0JHtn4pYqcXwRtLvElQ7/nnjDu7iYG56H0+OhCd7d6Ug0IE3WcFpZozBKW2+80FUbv5QGk5AQ==
|
||||
|
||||
"@types/bytes@3.1.1":
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/bytes/-/bytes-3.1.1.tgz#67a876422e660dc4c10a27f3e5bcfbd5455f01d0"
|
||||
integrity sha512-lOGyCnw+2JVPKU3wIV0srU0NyALwTBJlVSx5DfMQOFuuohA8y9S8orImpuIQikZ0uIQ8gehrRjxgQC1rLRi11w==
|
||||
|
||||
"@types/cacheable-request@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976"
|
||||
|
@ -7049,6 +7054,11 @@ bytes@3.1.0, bytes@^3.0.0:
|
|||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
|
||||
|
||||
bytes@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a"
|
||||
integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==
|
||||
|
||||
cacache@^12.0.2:
|
||||
version "12.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.3.tgz#be99abba4e1bf5df461cd5a2c1071fc432573390"
|
||||
|
|
Loading…
Reference in a new issue