{"id":"46cc4ba95a4f5de3","repo":"evanw/esbuild","slug":"must-specify-both-key-and-certificate-for-https","errorCode":null,"errorMessage":"Must specify both key and certificate for HTTPS","messagePattern":"Must specify both key and certificate for HTTPS","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/api/serve_other.go","lineNumber":746,"sourceCode":"}\n\nfunc (ctx *internalContext) Serve(serveOptions ServeOptions) (ServeResult, error) {\n\tctx.mutex.Lock()\n\tdefer ctx.mutex.Unlock()\n\n\t// Ignore disposed contexts\n\tif ctx.didDispose {\n\t\treturn ServeResult{}, errors.New(\"Cannot serve a disposed context\")\n\t}\n\n\t// Don't allow starting serve mode multiple times\n\tif ctx.handler != nil {\n\t\treturn ServeResult{}, errors.New(\"Serve mode has already been enabled\")\n\t}\n\n\t// Don't allow starting serve mode multiple times\n\tif (serveOptions.Keyfile != \"\") != (serveOptions.Certfile != \"\") {\n\t\treturn ServeResult{}, errors.New(\"Must specify both key and certificate for HTTPS\")\n\t}\n\n\t// Validate the \"servedir\" path\n\tif serveOptions.Servedir != \"\" {\n\t\tif absPath, ok := ctx.realFS.Abs(serveOptions.Servedir); ok {\n\t\t\tserveOptions.Servedir = absPath\n\t\t} else {\n\t\t\treturn ServeResult{}, fmt.Errorf(\"Invalid serve path: %s\", serveOptions.Servedir)\n\t\t}\n\t}\n\n\t// Validate the \"fallback\" path\n\tif serveOptions.Fallback != \"\" {\n\t\tif absPath, ok := ctx.realFS.Abs(serveOptions.Fallback); ok {\n\t\t\tserveOptions.Fallback = absPath\n\t\t} else {\n\t\t\treturn ServeResult{}, fmt.Errorf(\"Invalid fallback path: %s\", serveOptions.Fallback)\n\t\t}","sourceCodeStart":728,"sourceCodeEnd":764,"githubUrl":"https://github.com/evanw/esbuild/blob/6ff1d8b0d8c134e867a397eef39702a223ebef9e/pkg/api/serve_other.go#L728-L764","documentation":"Returned by internalContext.Serve when exactly one of Keyfile / Certfile is set but not the other. HTTPS requires both a private key and a certificate; setting only one is treated as a misconfiguration and rejected before any listener is created. The XOR check is (Keyfile != \"\") != (Certfile != \"\").","triggerScenarios":"Calling ctx.Serve() with ServeOptions where Keyfile is set but Certfile is empty, or vice versa. The XOR guard at serve_other.go:745 returns the error.","commonSituations":"A config loader that reads TLS_KEY from env but TLS_CERT is missing; typo in option names; copying a config where one path was left as the placeholder; a conditional that sets the keyfile only under some flag.","solutions":["Provide both Keyfile and Certfile, or neither (for plain HTTP).","Validate that both TLS file env vars are present before calling Serve().","Check for typos: the option names are Keyfile and Certfile (note: key then cert).","Log the resolved key/cert paths right before Serve() to catch empty values."],"exampleFix":"// before\nctx.Serve({ port: 443, Keyfile: './key.pem' }); // Certfile missing\n\n// after\nctx.Serve({ port: 443, Keyfile: './key.pem', Certfile: './cert.pem' });","handlingStrategy":"validation","validationCode":"function validTLS(opts) { return (opts.Keyfile ? 1 : 0) + (opts.Certfile ? 1 : 0) !== 1; }\nif (!validTLS(serveOpts)) throw new Error('Provide both key and cert, or neither');","typeGuard":"function hasBothTLSFiles(opts) { return !!opts.Keyfile && !!opts.Certfile; }","tryCatchPattern":"try { await ctx.Serve(opts); } catch (e) { if (/Must specify both key and certificate/.test(e.message)) { /* supply the missing file */ } throw e; }","preventionTips":["Always set both Keyfile and Certfile together.","Read both from env and fail fast if either is missing.","Double-check option names (Keyfile, Certfile).","Log resolved TLS paths before Serve()."],"tags":["esbuild","serve","https","tls","configuration"],"analyzedSha":"6ff1d8b0d8c134e867a397eef39702a223ebef9e","analyzedAt":"2026-08-03T19:42:38.433Z","schemaVersion":2}