I found the cause. This may trip other people up too.
It turns out that if you put carriage returns in the code (e.g. to neaten up functions, brackets, IF statements etc), then the code is passed as valid, but some carriage return gets inserted into the generated url.
In my case the carriage return had got misplaced, the code was correct but the url generation failed.
Probably users will expect that either
a) both the code check and the URL generation fail together
b) the code check and the URL generation both succeed.
I doubt this will be a priority but worth leaving up in case anyone notices the same issue.
If you neaten a string (text) portion of your code using a Line Field (%0A is not CR), this LF is taken into account as part of the string, exactly like how a space would be handled.
A string portion is any part of your code enclosed within single or double quotes, or returned by any of AppSheet text functions.
I would be interested in seeing your neatened code please. Thank you.