I deleted the function but the route still answers with a 200
A catch-all route serves index.html for any unmatched path, so a deleted function now returns your homepage with status 200. The deploy worked. Check the body rather than the status: run curl on the route and if the output starts with
A route we deleted that kept answering
A serverless function was deleted from the repository, the site was redeployed, and the route it used to answer kept returning 200. Not an error, not a blank response, a clean success. Every check that was run reported the endpoint as present, so the reasonable conclusion was that the deploy had not taken, or that something was holding a cached copy of the function.
Neither was true. The function was gone. What was answering was the catch-all: any path that does not match a real file or a real function gets index.html with status 200. Deleting the route did not remove a route so much as hand it to the homepage. The route responds, the status is healthy, and the body is a web page rather than an API response.
This specifically bit a temporary helper function that had been deployed to mint an OAuth token and then deleted once it had done its job. Deleting it did not obviously look deleted, which is the worst property a removal can have when the thing removed existed to hand out credentials. The loop of redeploying, rechecking the status and getting 200 again ran times before anyone read the body.
This is the exact mirror of a more familiar failure, where a file that never deployed returns 200 because the same catch-all answers for it. Same mechanism, opposite expectation. In one direction you expect something to be there and a success code convinces you it is. In the other you expect something to be gone and a success code convinces you it is not.
The catch-all answers when the function is gone
A catch-all route exists for a good reason. Client side routing needs unknown paths to load the application shell so the app can read the URL and render the right view. To do that, the host answers unmatched paths with index.html and a 200, because a 404 would stop the app from ever booting.
That rule cannot tell the difference between a path the application will handle and a path that used to be a function. Once the function is deleted, its path is simply unmatched, and unmatched means homepage. The status code you get back is the status code of a successful homepage delivery, which is genuinely what happened.
The reason people go around the loop several times is that every instinct points somewhere else. A 200 on a route you deleted looks like a stale deployment, so the fix that suggests itself is another deploy. When that produces another 200, it looks like caching, so the next move is a purge or a cache bypass. Both explanations are coherent and both are wrong, and neither can be ruled out by looking at status codes, because the status code is the part that is lying.
The underlying error is treating an HTTP status as a statement about content. It is not. It is a statement about whether a request was answered. On a host with a catch-all, almost every request is answered.
Read the body, not the status code
Read the first bytes of the body instead of the status:
curl -s https://yourdomain.com/api/thing | head -c 80
If that output starts with <!DOCTYPE, the endpoint is gone. There is no ambiguity in that result and no second interpretation of it. An API route that answers with the opening of an HTML document is not an API route.
If you want the comparison rather than the inference, hash the two responses. A route that is byte identical to your homepage is your homepage:
curl -s https://yourdomain.com/ | md5sum
curl -s https://yourdomain.com/api/thing | md5sum
The same check works in reverse for anything that is supposed to exist. A data file, a text file and a JSON endpoint can all be verified by asking whether their body looks like the thing they claim to be rather than by asking whether the request succeeded.
Check the body, then stop redeploying
- Read the body before you change anything. If the first bytes are
<!DOCTYPE, the removal already worked and there is nothing to fix on the host. - Stop the redeploy loop as soon as the body check comes back as HTML. Further deploys cannot change this result, because the deploy is not what produced it.
- Write the body check into whatever script verifies your deploys, once for routes that must exist and once for routes that must be gone. A route that must be gone passes when its body starts with
<!DOCTYPE, and a route that must exist fails on exactly the same condition. - For a deleted helper that handled credentials, treat the credential as exposed until the body check confirms the route is not answering with its own output. Status alone cannot support that conclusion, so the check is not optional.
- If you need unmatched paths to fail honestly, add a real
404.htmlso the host stops serving the homepage for them, or scope the catch-all so it never covers API prefixes. Both options change what every unknown path on the site does, so make the change deliberately rather than as part of debugging one route. - Record the check as a rule, not a memory. The rule that covers both directions is: check the body, not the status.
Test removed routes after every deploy
Make every deploy verification assert on content. A list of paths with expected status codes will pass on a site where half the files are missing and the other half were deleted. A list of paths with an expected string in the body will not.
Keep an explicit list of routes that are supposed to be absent. Temporary functions are the ones that cause trouble, because they are created under time pressure, they often hold a secret, and nobody writes a test for something they intend to delete within the hour.
Name the ambiguity when you build any endpoint. If a route returning your homepage is indistinguishable from a route returning nothing, then the only signal that distinguishes them has to come from the body, and the endpoint should return something recognisable enough to grep for.
When a check surprises you twice in the same way, change the check rather than the deploy. Two identical unexpected results are evidence about your instrument.
Companion failure. The same root cause in the other direction, where a file that never deployed returns 200 and defeats every error check in your code, is covered at missing file returns 200 not 404.
What the deleted function already handed out
Confirming that the body is HTML tells you the route is not there. It does not tell you anything about what the function did while it existed. A helper that minted a token handed out something real, and deleting the helper does not reach the token.
The body check does not remove the catch-all or change any of its other consequences. Every unknown path on the site still answers 200 with the homepage, which remains a problem for search engines and for any code that fetches a file and trusts the status code.
It also does not make missing files visible on its own. Each path that matters needs its own assertion, and a check that only runs against routes you remember will keep passing while something you forgot returns the homepage.
Adding a 404.html or narrowing the catch-all changes the behaviour of client side routing on the paths it covers. If the application depends on unmatched paths loading the shell, that dependency has to be handled before the rule is tightened, and is the list to establish first.
Questions about deleted Pages Functions
Why does a route I deleted still return 200?
Because a catch-all route serves index.html for any path that does not match a real file or function. Once the function is gone its path is unmatched, so the host answers with your homepage and a success status.
How do I tell whether the endpoint is actually gone?
Read the body rather than the status. Run curl -s https://yourdomain.com/api/thing | head -c 80, and if the output starts with
Is this a caching problem?
Usually not, though it looks exactly like one, which is why the loop of redeploying and rechecking runs several times. A cached response and a catch-all response both return 200, so only the body distinguishes them.
What is the general rule here?
Check the body, not the status. A route that returns your homepage HTML is a route that does not exist, whether you were expecting a file to be there or expecting it to be gone.
Does this matter for a temporary function that handled credentials?
Yes. A temporary helper deployed to mint an OAuth token and then deleted did not obviously look deleted, so the removal cannot be confirmed from the status code. Confirm it from the body before treating the credential as no longer reachable.
Where is the same problem in the other direction documented?
At /missing-file-returns-200-not-404, which covers a file that never deployed returning 200 and defeating the error checks in your own code. Same root cause, opposite expectation.