Skip to content

Commit

Permalink
Don’t show long internal Rust stack traces to users (#67678)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
2 people authored and ForsakenHarmony committed Aug 14, 2024
1 parent 54cf395 commit ad39dd6
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 10 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/next-swc/crates/napi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
83 changes: 76 additions & 7 deletions packages/next-swc/crates/napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -73,19 +76,85 @@ shadow_rs::shadow!(build);
static ALLOC: turbopack_binding::turbo::malloc::TurboMalloc =
turbopack_binding::turbo::malloc::TurboMalloc;

static LOG_THROTTLE: Mutex<Option<Instant>> = 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::<Vec<_>>()
};

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]
Expand Down
5 changes: 4 additions & 1 deletion packages/next/src/build/swc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
})
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 17 additions & 1 deletion packages/next/src/server/dev/turbopack-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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') {
Expand Down

0 comments on commit ad39dd6

Please sign in to comment.