{"record":{"id":"ba174b210aab1d08","repo":"grafana/k6","slug":"urltemplate-must-contain-key-placeholder","errorCode":null,"errorMessage":"urlTemplate must contain {key} placeholder","messagePattern":"urlTemplate must contain (.+?) placeholder","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/secretsource/url/url.go","lineNumber":525,"sourceCode":"\t\treturn extConfig{}, fmt.Errorf(\"failed to read config file: %w\", err)\n\t}\n\n\tvar fileCfg extConfig\n\tif err := json.Unmarshal(configData, &fileCfg); err != nil {\n\t\treturn extConfig{}, fmt.Errorf(\"failed to parse JSON config: %w\", err)\n\t}\n\n\treturn fileCfg, nil\n}\n\nfunc validateURLTemplate(urlTemplate string) error {\n\tif urlTemplate == \"\" {\n\t\treturn errMissingURLTemplate\n\t}\n\n\t// Require {key} placeholder to differentiate between secrets\n\tif !strings.Contains(urlTemplate, \"{key}\") {\n\t\treturn errors.New(\"urlTemplate must contain {key} placeholder\")\n\t}\n\n\t// Replace {key} placeholder with a dummy value for validation\n\ttestURL := strings.ReplaceAll(urlTemplate, \"{key}\", \"test\")\n\tparsedURL, err := url.Parse(testURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"urlTemplate is not a valid URL: %w\", err)\n\t}\n\n\t// Require absolute URL with scheme\n\tif parsedURL.Scheme == \"\" {\n\t\treturn errors.New(\"urlTemplate must be an absolute URL with a scheme (e.g., https://...)\")\n\t}\n\n\treturn nil\n}\n\nfunc getConfig(arg string, fs fsext.Fs, env map[string]string) (extConfig, error) {","sourceCodeStart":507,"sourceCodeEnd":543,"githubUrl":"https://github.com/grafana/k6/blob/93accf6570dcd306ca5e99cc44c393ee3797761b/internal/secretsource/url/url.go#L507-L543","documentation":"k6's 'url' secret source fetches each secret over HTTP by substituting the secret's name into a URL template. validateURLTemplate() rejects any template that lacks the literal '{key}' placeholder, because without it every secret would resolve to the same URL and individual secrets could not be addressed. The check runs at startup inside getConfig()/validateConfig(), before any test code executes.","triggerScenarios":"Configuring the url secret source via K6_SECRET_SOURCE_URL_URL_TEMPLATE, an inline CLI argument, or a JSON config file whose urlTemplate value has no '{key}' substring, e.g. 'https://vault.example.com/secrets/mysecret'.","commonSituations":"Hard-coding one secret's URL instead of templating it; copying an example that used a different placeholder name such as '{name}' or '$key'; URL-encoding the braces (%7Bkey%7D) so the literal '{key}' no longer matches.","solutions":["Put the literal {key} placeholder where the secret name belongs: K6_SECRET_SOURCE_URL_URL_TEMPLATE='https://vault.example.com/api/secrets/{key}'","Verify before running k6: echo \"$K6_SECRET_SOURCE_URL_URL_TEMPLATE\" | grep -q '{key}' || echo 'missing {key}'","If the backend has no per-key URL pattern, switch to a secret source that fits (file, env) instead of url"],"exampleFix":"# before\nK6_SECRET_SOURCE_URL_URL_TEMPLATE='https://vault.example.com/secrets/mytoken'\n\n# after\nK6_SECRET_SOURCE_URL_URL_TEMPLATE='https://vault.example.com/secrets/{key}'","handlingStrategy":"validation","validationCode":"#!/usr/bin/env bash\ntpl=\"$K6_SECRET_SOURCE_URL_URL_TEMPLATE\"\n[ -n \"$tpl\" ] && [[ \"$tpl\" == *'{key}'* ]] || { echo 'urlTemplate must contain {key}'; exit 1; }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep urlTemplate values in one place (CI variable or config file) instead of duplicating them","Add a CI lint step that greps the template for the literal {key}","Document the placeholder convention next to the secret source setup"],"tags":["config","secrets","url","validation","startup"],"backgroundTag":null,"analyzedSha":"93accf6570dcd306ca5e99cc44c393ee3797761b","analyzedAt":"2026-08-15T21:23:27.118Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}