The checkout team applied their HTTPRoute for checkout.lab an hour ago. The platform team says the shared Gateway is healthy. Both look right:
$ kubectl get gateway -A
NAMESPACE NAME CLASS ADDRESS PROGRAMMED AGE
gw-shared edge nginx 10.64.0.240 True 60m
$ kubectl get httproute -A
NAMESPACE NAME HOSTNAMES AGE
checkout checkout ["checkout.lab"] 60mAnd users get a 404:
$ IP=$(kubectl get gateway edge -n gw-shared -o jsonpath='{.status.addresses[0].value}'); echo $IP
10.64.0.240
$ curl -si -H 'Host: checkout.lab' http://$IP/ | head -5
HTTP/1.1 404 Not Found
Server: nginx
Date: Tue, 22 Sep 2026 20:00:04 GMT
Content-Type: text/html
Content-Length: 146kubectl get httproute shows no status column at all. "It exists" is all it tells you.
What is happening: a route has to be accepted by its parent
In Gateway API, three parties own three objects. The platform team owns the Gateway and decides which routes may attach to each listener. The app team owns the HTTPRoute and names the Gateway it wants (parentRefs). The owner of the backend Service decides who may point at it from another namespace (a ReferenceGrant).
A route only carries traffic when its parent Gateway accepts it, and only reaches its backend when every reference resolves. Both answers are written into the route's status, per parent, as conditions with a reason:
| condition | reason | meaning |
|---|---|---|
Accepted=False | NoMatchingParent | the parentRefs name, namespace or sectionName matches no Gateway or listener |
Accepted=False | NotAllowedByListeners | the listener's allowedRoutes does not admit this namespace or kind |
Accepted=False | NoMatchingListenerHostname | the route's hostnames and the listener's hostname do not intersect |
ResolvedRefs=False | BackendNotFound | the Service in backendRefs does not exist |
ResolvedRefs=False | RefNotPermitted | the backend is in another namespace and no ReferenceGrant there allows it |
| (no status at all) | no controller handles the Gateway's class |
The first False is your problem. Fixing it often reveals the next one, and this route had three.
Diagnosis
1. Read the route's conditions
One line per condition, without scrolling through describe:
$ kubectl get httproute checkout -n checkout -o jsonpath='{range .status.parents[*].conditions[*]}{.type}={.status} {.reason}{"\n"}{end}'
Accepted=False NoMatchingParent
ResolvedRefs=True ResolvedRefsdescribe gives the message too:
$ kubectl describe httproute checkout -n checkout | grep -E "Reason|Message|Section"
SectionName: http
Message: No listener with name "http" in Gateway gw-shared/edge
Reason: NoMatchingParent
Message: All references are resolved
Reason: ResolvedRefs
SectionName: httpThe route asks for a listener called http. What does the Gateway have?
$ kubectl get gateway edge -n gw-shared -o jsonpath='{.spec.listeners[*].name}'; echo
webFix the route (the app team's object), or drop sectionName and attach to every listener that allows it:
$ kubectl patch httproute checkout -n checkout --type=json -p '[{"op":"replace","path":"/spec/parentRefs/0/sectionName","value":"web"}]'
httproute.gateway.networking.k8s.io/checkout patched
$ kubectl describe httproute checkout -n checkout | grep -E "Reason|Message"
Message: Route is not allowed by any listener
Reason: NotAllowedByListeners
Message: Backend ref to Service payments/payments-api not permitted by any ReferenceGrant
Reason: RefNotPermittedProgress: two new reasons.
2. NotAllowedByListeners: the Gateway's admission rule
$ kubectl get gateway edge -n gw-shared -o jsonpath='{.spec.listeners[0].allowedRoutes}'; echo
{"namespaces":{"from":"Same"}}from: Same is the default: a listener only admits routes from the Gateway's own namespace. A shared Gateway has to opt namespaces in. from: All opens it to everyone, so a label selector is the safer choice. This is the platform team's change:
$ kubectl patch gateway edge -n gw-shared --type=json -p '[{"op":"replace","path":"/spec/listeners/0/allowedRoutes","value":{"namespaces":{"from":"Selector","selector":{"matchLabels":{"gateway-access":"edge"}}}}}]'
gateway.gateway.networking.k8s.io/edge patched
$ kubectl label ns checkout gateway-access=edge
namespace/checkout labeled
$ kubectl get httproute checkout -n checkout -o jsonpath='{range .status.parents[*].conditions[*]}{.type}={.status} {.reason}{"\n"}{end}'
Accepted=True Accepted
ResolvedRefs=False RefNotPermittedThe route is attached now. The Gateway's listener status counts it (kubectl get gateway edge -n gw-shared -o jsonpath='{.status.listeners[0].attachedRoutes}' went from 0 to 1). But the users' 404 has become something else:
$ curl -si -H 'Host: checkout.lab' http://$IP/ | head -5
HTTP/1.1 500 Internal Server Error
Server: nginx
Date: Tue, 22 Sep 2026 20:00:12 GMT
Content-Type: text/html
Content-Length: 170The spec says a rule whose backend cannot be used answers 500. That is a useful signal on its own: a 500 from the Gateway with no request reaching the app usually means an invalid backend reference.
3. RefNotPermitted: the backend's namespace must agree
The route in checkout points at payments/payments-api. Cross-namespace references are refused unless the target namespace publishes a grant:
$ kubectl get referencegrant -A
No resources foundThe payments team adds one, in payments:
apiVersion: gateway.networking.k8s.io/v1
kind: ReferenceGrant
metadata:
name: from-checkout
namespace: payments
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: checkout
to:
- group: ""
kind: Service(ReferenceGrant is v1 since Gateway API 1.5. v1beta1 is still served, and older guides use it.)
$ kubectl apply -f payments-grant.yaml
referencegrant.gateway.networking.k8s.io/from-checkout created
$ kubectl get httproute checkout -n checkout -o jsonpath='{range .status.parents[*].conditions[*]}{.type}={.status} {.reason}{"\n"}{end}'
Accepted=True Accepted
ResolvedRefs=True ResolvedRefs
$ curl -s -H 'Host: checkout.lab' http://$IP/hostname; echo
payments-api-5hxbk9zv9j-k24zhThree causes, three owners, one condition each. That is the point of status in Gateway API: every "the YAML looks fine" ends at a named reason.
When the conditions are all True and it still fails
Then the route is fine and the problem is further along. Check the Service's endpoints (a Service with no endpoints gives a 503), the request's Host header against hostnames, and the match order. In a service mesh, the sidecar's response flag tells you who said no. If the Gateway has no status at all, look at kubectl get gatewayclass: a class that no controller handles stays Pending for ever. For HTTPS listeners, the certificate is one more reference that has to resolve, and cert-manager has its own way of getting stuck.
Keeping it from coming back
- Read status in the pipeline. After applying a route, run the jsonpath one-liner above and fail the deploy unless both
AcceptedandResolvedRefsareTrue. A deploy that "succeeds" withAccepted=Falseis a silent outage. - Template the defaults. Give app teams a route template without
sectionNameunless they need one, and document the Gateway's admission label next to the Gateway. - Ship the ReferenceGrant with the Service, owned by the backend team, so cross-namespace access is reviewed where the data lives.
- Coming from Ingress? An Ingress has none of these conditions, which is why the same mistakes there end as a plain 404 (or, on OpenShift, "Application is not available").
ingress2gatewayconverts the objects. The status is what you gain.
Practise it
The incident "the new checkout route is ignored" in the chapter Kubernetes: Ingress, Gateway API & Service Mesh is this exact route, with the three owners' objects to fix and a curl that must reach a payments pod. The drill The route is ignored - which object is wrong? builds a different broken route on every round, from a misspelled class to a missing grant.