evanw/esbuild · error
Invalid path suffix returned from plugin (must start with…
Error message
Invalid path suffix %q returned from plugin (must start with "?" or "#")
What it means
When an esbuild plugin's OnResolve callback returns a suffix field, esbuild requires it to start with '?' (query string) or '#' (hash fragment). This restriction exists to match esbuild's internal handling of external path suffixes. Any other character as the first byte triggers this error.
Solutions
- Prefix the suffix with '?' for query strings (e.g. '?version=1')
- Prefix the suffix with '#' for hash fragments (e.g. '#section')
- Omit the suffix field entirely if your plugin does not need it
Example fix
// before
onResolve: () => ({ path: resolved, suffix: 'v=2' })
// after
onResolve: () => ({ path: resolved, suffix: '?v=2' }) Defensive patterns
Strategy: validation
Validate before calling
function validateOnResolveResult(result) {
if (result.suffix && result.suffix.length > 0) {
if (result.suffix[0] !== '?' && result.suffix[0] !== '#') {
throw new Error(`Suffix must start with '?' or '#', got: ${result.suffix}`)
}
}
return result
}
// In plugin setup:
onResolve: { filter: '.*' },
callback: (args) => {
const result = { path: resolvePath(args) }
if (args.query) result.suffix = '?' + args.query
return validateOnResolveResult(result)
} Prevention
- Always prefix suffix values with '?' for queries or '#' for hashes
- Only set the suffix field when you actually need query/hash suffixes
- Add a validation wrapper around onResolve return values during development
When it happens
Trigger: A plugin's onResolve callback returns { path, suffix: 'version=1' } where the suffix does not start with '?' or '#'.
Common situations: Plugin authors who misunderstand the suffix field's purpose and return arbitrary strings, or who forget to prefix the suffix with '?' or '#'.
Related errors
- Expected onResolve() callback in plugin
- Invalid charset
- Invalid color
- Invalid engine name
- Invalid format
AI-assisted analysis of evanw/esbuild@f6058f8364 (2026-08-09).
Data as JSON: /api/errors/1cab14cb54d00948.
Report an issue: GitHub.
Appendix: source
Thrown at pkg/api/api_impl.go:1979
Filter: filter,
Namespace: options.Namespace,
Callback: func(args config.OnResolveArgs) (result config.OnResolveResult) {
response, err := callback(OnResolveArgs{
Path: args.Path,
Importer: args.Importer.Text,
Namespace: args.Importer.Namespace,
ResolveDir: args.ResolveDir,
Kind: importKindToResolveKind(args.Kind),
PluginData: args.PluginData,
With: args.With.DecodeIntoMap(),
})
result.PluginName = response.PluginName
result.AbsWatchFiles = impl.validatePathsArray(response.WatchFiles, "watch file")
result.AbsWatchDirs = impl.validatePathsArray(response.WatchDirs, "watch directory")
// Restrict the suffix to start with "?" or "#" for now to match esbuild's behavior
if err == nil && response.Suffix != "" && response.Suffix[0] != '?' && response.Suffix[0] != '#' {
err = fmt.Errorf("Invalid path suffix %q returned from plugin (must start with \"?\" or \"#\")", response.Suffix)
}
if err != nil {
result.ThrownError = err
return
}
result.Path = logger.Path{
Text: response.Path,
Namespace: response.Namespace,
IgnoredSuffix: response.Suffix,
}
result.External = response.External
result.IsSideEffectFree = response.SideEffects == SideEffectsFalse
result.PluginData = response.PluginData
// Convert log messages
result.Msgs = convertErrorsAndWarningsToInternal(response.Errors, response.Warnings)View on GitHub (pinned to f6058f8364)