Skip to content

Commit 050bdf1

Browse files
authored
[backport] Turbopack: throw large static metadata error earlier (#83816)
1 parent 1f6ea09 commit 050bdf1

File tree

3 files changed

+92
-27
lines changed

3 files changed

+92
-27
lines changed

crates/next-core/src/next_app/metadata/route.rs

Lines changed: 88 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
use anyhow::{Ok, Result, bail};
66
use base64::{display::Base64Display, engine::general_purpose::STANDARD};
77
use indoc::{formatdoc, indoc};
8-
use turbo_rcstr::rcstr;
8+
use turbo_rcstr::{RcStr, rcstr};
99
use turbo_tasks::Vc;
1010
use turbo_tasks_fs::{self, File, FileContent, FileSystemPath};
1111
use turbopack::ModuleAssetContext;
1212
use turbopack_core::{
13-
asset::AssetContent, file_source::FileSource, source::Source, virtual_source::VirtualSource,
13+
asset::AssetContent,
14+
file_source::FileSource,
15+
issue::{Issue, IssueExt, IssueSeverity, IssueStage, OptionStyledString, StyledString},
16+
source::Source,
17+
virtual_source::VirtualSource,
1418
};
1519
use turbopack_ecmascript::utils::StringifyJs;
1620

@@ -133,8 +137,6 @@ async fn static_route_source(mode: NextMode, path: FileSystemPath) -> Result<Vc<
133137
let stem = path.file_stem();
134138
let stem = stem.unwrap_or_default();
135139

136-
let content_type = get_content_type(path.clone()).await?;
137-
138140
let cache_control = if stem == "favicon" {
139141
CACHE_HEADER_REVALIDATE
140142
} else if mode.is_production() {
@@ -143,16 +145,40 @@ async fn static_route_source(mode: NextMode, path: FileSystemPath) -> Result<Vc<
143145
CACHE_HEADER_NONE
144146
};
145147

146-
let original_file_content_b64 = get_base64_file_content(path.clone()).await?;
147-
148148
let is_twitter = stem == "twitter-image";
149149
let is_open_graph = stem == "opengraph-image";
150+
151+
let content_type = get_content_type(path.clone()).await?;
152+
let original_file_content_b64;
153+
150154
// Twitter image file size limit is 5MB.
151155
// General Open Graph image file size limit is 8MB.
152156
// x-ref: https://developer.x.com/en/docs/x-for-websites/cards/overview/summary
153157
// x-ref(facebook): https://developers.facebook.com/docs/sharing/webmasters/images
154-
let file_size_limit = if is_twitter { 5 } else { 8 };
155-
let img_name = if is_twitter { "Twitter" } else { "Open Graph" };
158+
let file_size_limit_mb = if is_twitter { 5 } else { 8 };
159+
if (is_twitter || is_open_graph)
160+
&& let Some(content) = path.read().await?.as_content()
161+
&& let file_size = content.content().to_bytes().len()
162+
&& file_size > (file_size_limit_mb * 1024 * 1024)
163+
{
164+
StaticMetadataFileSizeIssue {
165+
img_name: if is_twitter {
166+
rcstr!("Twitter")
167+
} else {
168+
rcstr!("Open Graph")
169+
},
170+
path: path.clone(),
171+
file_size_limit_mb,
172+
file_size,
173+
}
174+
.resolved_cell()
175+
.emit();
176+
177+
// Don't inline huge string, just insert placeholder
178+
original_file_content_b64 = "".to_string();
179+
} else {
180+
original_file_content_b64 = get_base64_file_content(path.clone()).await?
181+
}
156182

157183
let code = formatdoc! {
158184
r#"
@@ -162,16 +188,6 @@ async fn static_route_source(mode: NextMode, path: FileSystemPath) -> Result<Vc<
162188
const cacheControl = {cache_control}
163189
const buffer = Buffer.from({original_file_content_b64}, 'base64')
164190
165-
if ({is_twitter} || {is_open_graph}) {{
166-
const fileSizeInMB = buffer.byteLength / 1024 / 1024
167-
if (fileSizeInMB > {file_size_limit}) {{
168-
throw new Error('File size for {img_name} image {path} exceeds {file_size_limit}MB. ' +
169-
`(Current: ${{fileSizeInMB.toFixed(2)}}MB)\n` +
170-
'Read more: https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#image-files-jpg-png-gif'
171-
)
172-
}}
173-
}}
174-
175191
export function GET() {{
176192
return new NextResponse(buffer, {{
177193
headers: {{
@@ -186,11 +202,6 @@ async fn static_route_source(mode: NextMode, path: FileSystemPath) -> Result<Vc<
186202
content_type = StringifyJs(&content_type),
187203
cache_control = StringifyJs(cache_control),
188204
original_file_content_b64 = StringifyJs(&original_file_content_b64),
189-
is_twitter = is_twitter,
190-
is_open_graph = is_open_graph,
191-
file_size_limit = file_size_limit,
192-
img_name = img_name,
193-
path = StringifyJs(&path.value_to_string().await?),
194205
};
195206

196207
let file = File::from(code);
@@ -412,3 +423,57 @@ async fn dynamic_image_route_source(path: FileSystemPath) -> Result<Vc<Box<dyn S
412423

413424
Ok(Vc::upcast(source))
414425
}
426+
427+
#[turbo_tasks::value(shared)]
428+
struct StaticMetadataFileSizeIssue {
429+
img_name: RcStr,
430+
path: FileSystemPath,
431+
file_size: usize,
432+
file_size_limit_mb: usize,
433+
}
434+
435+
#[turbo_tasks::value_impl]
436+
impl Issue for StaticMetadataFileSizeIssue {
437+
fn severity(&self) -> IssueSeverity {
438+
IssueSeverity::Error
439+
}
440+
441+
#[turbo_tasks::function]
442+
fn title(&self) -> Vc<StyledString> {
443+
StyledString::Text(rcstr!("Static metadata file size exceeded")).cell()
444+
}
445+
446+
#[turbo_tasks::function]
447+
fn stage(&self) -> Vc<IssueStage> {
448+
IssueStage::ProcessModule.into()
449+
}
450+
451+
#[turbo_tasks::function]
452+
fn file_path(&self) -> Vc<FileSystemPath> {
453+
self.path.clone().cell()
454+
}
455+
456+
#[turbo_tasks::function]
457+
async fn description(&self) -> Result<Vc<OptionStyledString>> {
458+
Ok(Vc::cell(Some(
459+
StyledString::Text(
460+
format!(
461+
"File size for {} image \"{}\" exceeds {}MB. (Current: {:.1}MB)",
462+
self.img_name,
463+
self.path.value_to_string().await?,
464+
self.file_size_limit_mb,
465+
(self.file_size as f32) / 1024.0 / 1024.0
466+
)
467+
.into(),
468+
)
469+
.resolved_cell(),
470+
)))
471+
}
472+
473+
#[turbo_tasks::function]
474+
fn documentation_link(&self) -> Vc<RcStr> {
475+
Vc::cell(rcstr!(
476+
"https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image#image-files-jpg-png-gif"
477+
))
478+
}
479+
}

test/production/app-dir/metadata-img-too-large/opengraph-image/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ describe('app-dir - metadata-img-too-large opengraph-image', () => {
1414
await next.build()
1515

1616
const regex = isTurbopack
17-
? // in Turbopack, the path is simplified as [project]/...
18-
/Error: File size for Open Graph image "\[project\]\/app\/opengraph-image\.png" exceeds 8MB/
17+
? // in Turbopack, the path is simplified as [project]/.... It's also thrown earlier, so the prefix is slightly different.
18+
/File size for Open Graph image "\[project\]\/app\/opengraph-image\.png" exceeds 8MB/
1919
: /Error: File size for Open Graph image ".*\/app\/opengraph-image\.png" exceeds 8MB/
2020

2121
expect(next.cliOutput).toMatch(regex)

test/production/app-dir/metadata-img-too-large/twitter-image/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ describe('app-dir - metadata-img-too-large twitter-image', () => {
1414
await next.build()
1515

1616
const regex = isTurbopack
17-
? // in Turbopack, the path is simplified as [project]/...
18-
/Error: File size for Twitter image "\[project\]\/app\/twitter-image\.png" exceeds 5MB/
17+
? // in Turbopack, the path is simplified as [project]/.... It's also thrown earlier, so the prefix is slightly different.
18+
/File size for Twitter image "\[project\]\/app\/twitter-image\.png" exceeds 5MB/
1919
: /Error: File size for Twitter image ".*\/app\/twitter-image\.png" exceeds 5MB/
2020

2121
expect(next.cliOutput).toMatch(regex)

0 commit comments

Comments
 (0)