From ad39dd621aa3ff7a166da8ab198a0747b5994c99 Mon Sep 17 00:00:00 2001 From: Will Binns-Smith Date: Tue, 16 Jul 2024 16:43:03 -0700 Subject: [PATCH] =?UTF-8?q?Don=E2=80=99t=20show=20long=20internal=20Rust?= =?UTF-8?q?=20stack=20traces=20to=20users=20(#67678)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This: - Writes Turbopack internal errors to a file, `.next/fatal.log` - [x] Automatically truncate this file to prevent it from growing infinitely - Shows a simplified message to users when an internal Turbopack error occurs, directing them to file an issue with the content of the log - Adds a panic handler on the Rust side in release builds that similarly shows a simplified message --------- Co-authored-by: Benjamin Woodruff --- Cargo.lock | 1 + packages/next-swc/crates/napi/Cargo.toml | 1 + packages/next-swc/crates/napi/src/lib.rs | 83 +++++++++++++++++-- packages/next/src/build/swc/index.ts | 5 +- .../internal/helpers/nodeStackFrames.ts | 10 +++ .../next/src/server/dev/turbopack-utils.ts | 18 +++- .../lib/router-utils/setup-dev-bundler.ts | 9 +- 7 files changed, 117 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d55f55ad9ce73..de885ce55f808 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3079,6 +3079,7 @@ dependencies = [ "next-core", "next-custom-transforms", "once_cell", + "owo-colors 3.5.0", "rand", "serde", "serde_json", diff --git a/packages/next-swc/crates/napi/Cargo.toml b/packages/next-swc/crates/napi/Cargo.toml index 143fb8037a50f..b33330ba90255 100644 --- a/packages/next-swc/crates/napi/Cargo.toml +++ b/packages/next-swc/crates/napi/Cargo.toml @@ -51,6 +51,7 @@ backtrace = "0.3" fxhash = "0.2.1" dhat = { workspace = true, optional = true } indexmap = { workspace = true } +owo-colors = { workspace = true } napi = { version = "2", default-features = false, features = [ "napi3", "serde-json", diff --git a/packages/next-swc/crates/napi/src/lib.rs b/packages/next-swc/crates/napi/src/lib.rs index 0b3aae9c02c82..58ba389671457 100644 --- a/packages/next-swc/crates/napi/src/lib.rs +++ b/packages/next-swc/crates/napi/src/lib.rs @@ -35,13 +35,16 @@ extern crate napi_derive; use std::{ env, + io::prelude::*, panic::set_hook, - sync::{Arc, Once}, + sync::{Arc, Mutex, Once}, + time::Instant, }; use backtrace::Backtrace; use fxhash::FxHashSet; use napi::bindgen_prelude::*; +use owo_colors::OwoColorize; use turbopack_binding::swc::core::{ base::{Compiler, TransformOutput}, common::{FilePathMapping, SourceMap}, @@ -73,19 +76,85 @@ shadow_rs::shadow!(build); static ALLOC: turbopack_binding::turbo::malloc::TurboMalloc = turbopack_binding::turbo::malloc::TurboMalloc; +static LOG_THROTTLE: Mutex> = Mutex::new(None); +static LOG_FILE_PATH: &str = ".next/turbopack.log"; + #[cfg(feature = "__internal_dhat-heap")] #[global_allocator] static ALLOC: dhat::Alloc = dhat::Alloc; #[cfg(not(target_arch = "wasm32"))] #[napi::module_init] + fn init() { - if cfg!(debug_assertions) || env::var("SWC_DEBUG").unwrap_or_default() == "1" { - set_hook(Box::new(|panic_info| { - let backtrace = Backtrace::new(); - println!("Panic: {:?}\nBacktrace: {:?}", panic_info, backtrace); - })); - } + use std::{fs::OpenOptions, io}; + + set_hook(Box::new(|panic_info| { + // hold open this mutex guard to prevent concurrent writes to the file! + let mut last_error_time = LOG_THROTTLE.lock().unwrap(); + if let Some(last_error_time) = last_error_time.as_ref() { + if last_error_time.elapsed().as_secs() < 1 { + // Throttle panic logging to once per second + return; + } + } + *last_error_time = Some(Instant::now()); + + let backtrace = Backtrace::new(); + let info = format!("Panic: {}\nBacktrace: {:?}", panic_info, backtrace); + if cfg!(debug_assertions) || env::var("SWC_DEBUG") == Ok("1".to_string()) { + eprintln!("{}", info); + } else { + let size = std::fs::metadata(LOG_FILE_PATH).map(|m| m.len()); + if let Ok(size) = size { + if size > 512 * 1024 { + // Truncate the earliest error from log file if it's larger than 512KB + let new_lines = { + let log_read = OpenOptions::new() + .read(true) + .open(LOG_FILE_PATH) + .unwrap_or_else(|_| panic!("Failed to open {}", LOG_FILE_PATH)); + + io::BufReader::new(&log_read) + .lines() + .skip(1) + .skip_while(|line| match line { + Ok(line) => !line.starts_with("Panic:"), + Err(_) => false, + }) + .collect::>() + }; + + let mut log_write = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(LOG_FILE_PATH) + .unwrap_or_else(|_| panic!("Failed to open {}", LOG_FILE_PATH)); + + for line in new_lines { + match line { + Ok(line) => { + writeln!(log_write, "{}", line).unwrap(); + } + Err(_) => { + break; + } + } + } + } + } + + let mut log_file = OpenOptions::new() + .create(true) + .append(true) + .open(LOG_FILE_PATH) + .unwrap_or_else(|_| panic!("Failed to open {}", LOG_FILE_PATH)); + + writeln!(log_file, "{}", info).unwrap(); + eprintln!("{}: An unexpected Turbopack error occurred. Please report the content of {} to https://github.com/vercel/next.js/issues/new", "FATAL".red().bold(), LOG_FILE_PATH); + } + })); } #[inline] diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index eee80b15ea525..b1f5b9aac5378 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -23,6 +23,7 @@ import { import type { PageExtensions } from '../page-extensions-type' import type { __ApiPreviewProps } from '../../server/api-utils' import { getReactCompilerLoader } from '../get-babel-loader-config' +import { TurbopackInternalError } from '../../server/dev/turbopack-utils' const nextVersion = process.env.__NEXT_VERSION as string @@ -796,7 +797,9 @@ function bindingToApi( try { return await fn() } catch (nativeError: any) { - throw new Error(nativeError.message, { cause: nativeError }) + throw new TurbopackInternalError(nativeError.message, { + cause: nativeError, + }) } } diff --git a/packages/next/src/client/components/react-dev-overlay/internal/helpers/nodeStackFrames.ts b/packages/next/src/client/components/react-dev-overlay/internal/helpers/nodeStackFrames.ts index 1dc9bef8e1c54..c3e8ef1db45ef 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/helpers/nodeStackFrames.ts +++ b/packages/next/src/client/components/react-dev-overlay/internal/helpers/nodeStackFrames.ts @@ -25,6 +25,16 @@ export function getFilesystemFrame(frame: StackFrame): StackFrame { } export function getServerError(error: Error, type: ErrorSourceType): Error { + if (error.name === 'TurbopackInternalError') { + // If this is an internal Turbopack error we shouldn't show internal details + // to the user. These are written to a log file instead. + const turbopackInternalError = new Error( + 'An unexpected Turbopack error occurred. Please report the content of .next/turbopack.log to the Next.js team at https://github.com/vercel/next.js/issues/new' + ) + decorateServerError(turbopackInternalError, type) + return turbopackInternalError + } + let n: Error try { throw new Error(error.message) diff --git a/packages/next/src/server/dev/turbopack-utils.ts b/packages/next/src/server/dev/turbopack-utils.ts index d6701c15bd610..cf6b912777e80 100644 --- a/packages/next/src/server/dev/turbopack-utils.ts +++ b/packages/next/src/server/dev/turbopack-utils.ts @@ -44,7 +44,23 @@ export async function getTurbopackJsConfig( return jsConfig ?? { compilerOptions: {} } } -export class ModuleBuildError extends Error {} +// An error generated from emitted Turbopack issues. This can include build +// errors caused by issues with user code. +export class ModuleBuildError extends Error { + name = 'ModuleBuildError' +} + +// An error caused by an internal issue in Turbopack. These should be written +// to a log file and details should not be shown to the user. +export class TurbopackInternalError extends Error { + name = 'TurbopackInternalError' + cause: unknown + + constructor(message: string, cause: unknown) { + super(message) + this.cause = cause + } +} /** * Thin stopgap workaround layer to mimic existing wellknown-errors-plugin in webpack's build diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index 2f3ffebedd93c..7b2f5147307e0 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -79,7 +79,10 @@ import { createHotReloaderTurbopack } from '../../dev/hot-reloader-turbopack' import { getErrorSource } from '../../../shared/lib/error-source' import type { StackFrame } from 'next/dist/compiled/stacktrace-parser' import { generateEncryptionKeyBase64 } from '../../app-render/encryption-utils' -import { ModuleBuildError } from '../../dev/turbopack-utils' +import { + ModuleBuildError, + TurbopackInternalError, +} from '../../dev/turbopack-utils' import { isMetadataRoute } from '../../../lib/metadata/is-metadata-route' import { normalizeMetadataPageToRoute } from '../../../lib/metadata/get-metadata-route' @@ -1033,7 +1036,11 @@ function logError( type?: 'unhandledRejection' | 'uncaughtException' | 'warning' | 'app-dir' ) { if (err instanceof ModuleBuildError) { + // Errors that may come from issues from the user's code Log.error(err.message) + } else if (err instanceof TurbopackInternalError) { + // An internal Turbopack error that has been handled by next-swc, written + // to disk and a simplified message shown to user on the Rust side. } else if (type === 'warning') { Log.warn(err) } else if (type === 'app-dir') {