evanw/esbuild · error

Invalid path suffix %q returned from plugin (must start with

Error message

Invalid path suffix %q returned from plugin (must start with "?" or "#")

What it means

Returned from esbuild's internal OnResolve dispatch (pkg/api/api_impl.go) when a plugin's OnResolve callback sets a Suffix whose first character is neither '?' nor '#'. esbuild restricts path suffixes to query-string ('?...') or fragment ('#...') prefixes because that is the only form it can attach to a resolved path unambiguously. Any other suffix is rejected as a contract violation by the plugin.

Source

Thrown at pkg/api/api_impl.go:1954

		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 6ff1d8b0d8)

Solutions

  1. Ensure your plugin's OnResolve only ever returns a Suffix beginning with '?' (query) or '#' (fragment).
  2. If you need to attach extra data, encode it as a query string: Suffix: '?foo=bar'.
  3. Strip any leading non-?/# characters from the suffix before returning it.
  4. Add a unit test in your plugin asserting the suffix starts with '?' or '#'.

Example fix

// before
onResolve({ filter: /.*/ }, args => ({
  path: args.path,
  suffix: '.cache' // invalid
}));

// after
onResolve({ filter: /.*/ }, args => ({
  path: args.path,
  suffix: '?cache=1' // valid query suffix
}));
Defensive patterns

Strategy: validation

Validate before calling

function validSuffix(s) { return s === '' || s[0] === '?' || s[0] === '#'; }
// in plugin:
const suffix = computeSuffix();
if (!validSuffix(suffix)) throw new Error('suffix must start with ? or #');
onResolveResult({ suffix });

Type guard

function isValidSuffix(s) { return typeof s === 'string' && (s.length === 0 || s[0] === '?' || s[0] === '#'); }

Prevention

When it happens

Trigger: A plugin's OnResolve result returns response.Suffix that is non-empty and does not start with '?' or '#'. The check at api_impl.go:1953 fires: err = fmt.Errorf("Invalid path suffix %q ..."). This is purely a plugin-author error, not a user-config error.

Common situations: A plugin author accidentally sets Suffix to something like '.js' or '?v=1#extra' starting with the wrong char; passing a full query incorrectly; copying the path (including its suffix) into the Suffix field instead of just the query/fragment portion; a plugin built against an older esbuild that had looser suffix handling.

Related errors


AI-assisted analysis of evanw/esbuild@6ff1d8b0d8 (2026-08-03). Data as JSON: /data/errors/1cab14cb54d00948.json. Report an issue: GitHub.