Having wasted several hours trying to work out what was wrong with my code, only to find it was nothing to do with that, I'm just recording this for later.
Firefox was giving me this error:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://localhost:4572/[...]. (Reason: CORS request did not succeed).
Seemed clear enough. So I spent several hours checking headers, adding debug into LocalStack (which I'm using to mimic S3), and the like.
Eventually it dawned on me that the 'network' tabe wasn't even showing the pre-flight OPTIONS request. So I did what I should have done some time back and tried another browser (Chrome), which worked fine.
Thanks to https://stackoverflow.com/posts/51888226/revisions I eventually worked out that it was because Firefox didn't accept the self-signed certificate on the local server. Nothing to do with CORS. (The server started off as HTTP, for that matter.)
So. Chrome is arguably doing the wrong thing by allowing an HTTP PUT from an HTTPS site without a murmur. But that error from Firefox is really misleading...
Hopefully this might help someone else, even if it's just me in six months time!