{"id":"1cab14cb54d00948","repo":"evanw/esbuild","slug":"invalid-path-suffix-q-returned-from-plugin-must","errorCode":null,"errorMessage":"Invalid path suffix %q returned from plugin (must start with \"?\" or \"#\")","messagePattern":"Invalid path suffix %q returned from plugin \\(must start with \"\\?\" or \"#\"\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/api/api_impl.go","lineNumber":1954,"sourceCode":"\t\tFilter:    filter,\n\t\tNamespace: options.Namespace,\n\t\tCallback: func(args config.OnResolveArgs) (result config.OnResolveResult) {\n\t\t\tresponse, err := callback(OnResolveArgs{\n\t\t\t\tPath:       args.Path,\n\t\t\t\tImporter:   args.Importer.Text,\n\t\t\t\tNamespace:  args.Importer.Namespace,\n\t\t\t\tResolveDir: args.ResolveDir,\n\t\t\t\tKind:       importKindToResolveKind(args.Kind),\n\t\t\t\tPluginData: args.PluginData,\n\t\t\t\tWith:       args.With.DecodeIntoMap(),\n\t\t\t})\n\t\t\tresult.PluginName = response.PluginName\n\t\t\tresult.AbsWatchFiles = impl.validatePathsArray(response.WatchFiles, \"watch file\")\n\t\t\tresult.AbsWatchDirs = impl.validatePathsArray(response.WatchDirs, \"watch directory\")\n\n\t\t\t// Restrict the suffix to start with \"?\" or \"#\" for now to match esbuild's behavior\n\t\t\tif err == nil && response.Suffix != \"\" && response.Suffix[0] != '?' && response.Suffix[0] != '#' {\n\t\t\t\terr = fmt.Errorf(\"Invalid path suffix %q returned from plugin (must start with \\\"?\\\" or \\\"#\\\")\", response.Suffix)\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tresult.ThrownError = err\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tresult.Path = logger.Path{\n\t\t\t\tText:          response.Path,\n\t\t\t\tNamespace:     response.Namespace,\n\t\t\t\tIgnoredSuffix: response.Suffix,\n\t\t\t}\n\t\t\tresult.External = response.External\n\t\t\tresult.IsSideEffectFree = response.SideEffects == SideEffectsFalse\n\t\t\tresult.PluginData = response.PluginData\n\n\t\t\t// Convert log messages\n\t\t\tresult.Msgs = convertErrorsAndWarningsToInternal(response.Errors, response.Warnings)","sourceCodeStart":1936,"sourceCodeEnd":1972,"githubUrl":"https://github.com/evanw/esbuild/blob/6ff1d8b0d8c134e867a397eef39702a223ebef9e/pkg/api/api_impl.go#L1936-L1972","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure your plugin's OnResolve only ever returns a Suffix beginning with '?' (query) or '#' (fragment).","If you need to attach extra data, encode it as a query string: Suffix: '?foo=bar'.","Strip any leading non-?/# characters from the suffix before returning it.","Add a unit test in your plugin asserting the suffix starts with '?' or '#'."],"exampleFix":"// before\nonResolve({ filter: /.*/ }, args => ({\n  path: args.path,\n  suffix: '.cache' // invalid\n}));\n\n// after\nonResolve({ filter: /.*/ }, args => ({\n  path: args.path,\n  suffix: '?cache=1' // valid query suffix\n}));","handlingStrategy":"validation","validationCode":"function validSuffix(s) { return s === '' || s[0] === '?' || s[0] === '#'; }\n// in plugin:\nconst suffix = computeSuffix();\nif (!validSuffix(suffix)) throw new Error('suffix must start with ? or #');\nonResolveResult({ suffix });","typeGuard":"function isValidSuffix(s) { return typeof s === 'string' && (s.length === 0 || s[0] === '?' || s[0] === '#'); }","tryCatchPattern":null,"preventionTips":["Plugin authors: only return suffixes starting with '?' or '#'.","Add unit tests asserting suffix format.","Encode extra data as a query string, never as an arbitrary string.","Validate suffix before returning from OnResolve."],"tags":["esbuild","plugin","onresolve","validation"],"analyzedSha":"6ff1d8b0d8c134e867a397eef39702a223ebef9e","analyzedAt":"2026-08-03T19:42:38.433Z","schemaVersion":2}