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

  1. Prefix the suffix with '?' for query strings (e.g. '?version=1')
  2. Prefix the suffix with '#' for hash fragments (e.g. '#section')
  3. 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

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


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)