{"record":{"id":"22b0fc741850c6a8","repo":"hashicorp/nomad","slug":"unable-to-determine-local-service-port-for-service","errorCode":null,"errorMessage":"unable to determine local service port for service check %s->%s->%s","messagePattern":"unable to determine local service port for service check (.+?)->(.+?)->(.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nomad/job_endpoint_hook_expose_check.go","lineNumber":216,"sourceCode":"\t\t\tTo:          -1,\n\t\t}\n\n\t\ttg.Networks[0].DynamicPorts = append(tg.Networks[0].DynamicPorts, port)\n\t\tcheck.PortLabel = port.Label\n\t}\n\n\t// Determine the local service port (i.e. what port the service is actually\n\t// listening to inside the network namespace).\n\t//\n\t// Similar logic exists in getAddress of client.go which is used for\n\t// creating check & service registration objects.\n\t//\n\t// The difference here is the address is predestined to be localhost since\n\t// it is binding inside the namespace.\n\tvar port int\n\tif mapping := tg.Networks.Port(s.PortLabel); mapping.Value <= 0 { // try looking up by port label\n\t\tif port, _ = strconv.Atoi(s.PortLabel); port <= 0 { // then try direct port value\n\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\"unable to determine local service port for service check %s->%s->%s\",\n\t\t\t\ttg.Name, s.Name, check.Name,\n\t\t\t)\n\t\t}\n\t} else {\n\t\tport = mapping.Value\n\t}\n\n\t// The Path, Protocol, and PortLabel are just copied over from the service\n\t// check definition.\n\treturn &structs.ConsulExposePath{\n\t\tPath:          check.Path,\n\t\tProtocol:      check.Protocol,\n\t\tLocalPathPort: port,\n\t\tListenerPort:  check.PortLabel,\n\t}, nil\n}\n","sourceCodeStart":198,"sourceCodeEnd":234,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/nomad/job_endpoint_hook_expose_check.go#L198-L234","documentation":"When mutating a job to add expose paths, Nomad must compute the local port that the Envoy proxy should listen on for an exposed check. It resolves the port via the group's network port label; if the label is not found in tg.Networks.Port() and the port label string itself is not a numeric port, no local port can be determined and the mutation fails.","triggerScenarios":"A group service with an expose=true check whose port_label references a dynamic label that does not exist in the group's network block, and is not a literal port number — typically when the group declares no network { port \"x\" {} } entry or the label is misspelled.","commonSituations":"Referencing a port defined at task level instead of group network level; typo in port_label; using a label that only exists after dynamic port allocation but declaring networks = [] with bridge mode and no explicit port stanza.","solutions":["Add a network { port \"<label>\" {} } stanza to the task group matching the check's port_label.","Correct the port_label spelling to match an existing group network port.","Set port_label to a literal numeric port value as a fallback."],"exampleFix":"// before\ngroup \"app\" {\n  network { mode = \"bridge\" } # no port \"api\" defined\n  service {\n    check { expose = true port_label = \"api\" }\n  }\n}\n// after\ngroup \"app\" {\n  network {\n    mode = \"bridge\"\n    port \"api\" {}\n  }\n  service {\n    check { expose = true port_label = \"api\" }\n  }\n}","handlingStrategy":"validation","validationCode":"// Verify each exposed check's port_label exists in group network ports or is numeric\nlabels := map[string]bool{}\nfor _, n := range tg.Networks {\n  for _, p := range n.ReservedPorts { labels[p.Label] = true }\n  for _, p := range n.DynamicPorts { labels[p.Label] = true }\n}\nfor _, s := range tg.Services {\n  for _, c := range s.Checks {\n    if c.Expose {\n      if !labels[s.PortLabel] && !isNumeric(s.PortLabel)\n        throw new Error(`exposed check on '${s.Name}' uses unresolvable port_label '${s.PortLabel}'`)\n    }\n  }\n}","typeGuard":"func resolvablePortLabel(tg *api.TaskGroup, label string) bool {\n\tfor _, n := range tg.Networks {\n\t\tfor _, p := range append(n.ReservedPorts, n.DynamicPorts...) {\n\t\t\tif p.Label == label {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\tif _, err := strconv.Atoi(label); err == nil {\n\t\treturn true\n\t}\n\treturn false\n}","tryCatchPattern":"// golang — Mutate failure surfaces as job submission/validation error\nif _, err := client.Jobs().Validate(job); err != nil {\n\tif strings.Contains(err.Error(), \"unable to determine local service port\") {\n\t\t// add a network port stanza matching the port_label\n\t}\n}","preventionTips":["Always declare a network { port \"<label>\" {} } for any port_label used by exposed checks.","Define ports at the group network level (task-level ports are not visible to the hook).","Check spelling of port_label against the group network block.","Use literal ports sparingly; prefer labeled dynamic ports declared explicitly."],"tags":["nomad","consul-connect","port-label","expose-check"],"backgroundTag":"unresolved-port-label","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}