Percent-Encode HTML Form Values so That Spaces Become ‘%20’ Instead of ‘+’
I had this great idea, using URL Schemes on macOS apps, assembled by very simple HTML files that contain a <form>
only and produce a GET request so that the data is part of the URL:
<form method="GET" action="thearchive://plugin/com.example.plugin/run">
<input type="text" name="text" />
<input type="submit" value="Send" />
</form>
To my dismay, I discovered that all browsers encode spaces in input
and textarea
elements as plusses: +
. Turns out that’s valid and expected.
So typing this value intp the input
hello world
becomes this query parameter:
?text=hello+world
If this is known, you can replace all occurrences of "+"
with a space, " "
, in the app and you’re fine.
Right?
Being naturally curious a trained programmer with at least mild OCD, I tested how literal plusses would be encoded.
This input:
hello + world
becomes this value:
?text=hello+++world
Now that’s not helpful at all. There’s no way to discern a plus from an encoded space!
If only browsers would URI-encode values so that spaces become %20
!
Without JavaScript, the situation is hopeless. I’m sorry.
With JavaScript, you can use the built-in encodeURIComponent
function to get percent-encoding. But you can’t use an XMLHTTPRequest
or the fetch
APIs to submit the form, because of Cross Origin Resource Sharing policies implemented in all modern browsers, aka “you can’t send a request to your app’s custom URL scheme”.
Javascript Copypasta That Doesn’t Work Here, but Could Work for You Elsewhere
If you’re coming from a web search, looking for how to assemble a request like this, the following boilerplate would work if you send it to an HTTP endpoint, not e.g. a custom URL scheme like thearchive://
.
I’m not sure about encoding the key, to be frank; but if you’re a noob like me, this may still come in handy to get you started.
let form = document.querySelector("form");
form.addEventListener("submit", (event) => {
event.preventDefault();
const formData = new FormData(form);
let params = [];
for (let [key, value] of formData.entries()) {
let encodedKey = encodeURIComponent(key);
let encodedValue = encodeURIComponent(value);
params.push(`${encodedKey}=${encodedValue}`);
}
const queryString = params.join('&');
var xhr = new XMLHttpRequest();
xhr.open(form.method, form.action);
xhr.send(queryString);
});
You could also send a FormData
object directly via both fetch
and XMLHttpRequest
s, I found out. But I haven’t tried to spend more time on this dead end.
Now that we’ve seen an implementation that took time to write but does not work for me here, let’s check out an implementation that does work.
Modify the FormData
Before It’s Sent By the Browser
I mentioned that you cand start a request from JavaScript because of CORS policies. But the browser is fine sending the form directly to any endpoint that you specify in the form’s action
attribute.
Instead of intercepting and replacing the submit functionality, this is how you can instead alter the form data that’ll be sent by the browser mid-flight:
let form = document.querySelector("form");
form.addEventListener("formdata", (e) => {
const formData = e.formData;
for (let [key, value] of formData.entries()) {
formData.set(key, encodeURIComponent(value));
}
});
Read more about the "formdata"
event on MDN. This is triggered before submit, and the result is then processed by the browser as the form’s data during the request. So you can write a decorator for the FormData
object constructor, more or less.
This implementation doesn’t need to construct its own request, so it’s progressively enhancing the HTML form by default. With JS disabled, users will send weirdly formatted space characters to the receiving app. At least nothing will be lost.
Send a Flag to Tell the App Whether Spaces are URI-Encoded
If you want to prevent the default browser behavior of using +
for spaces to reach your app in case JS is disabled, you need to discern un-decorated form data from decorated data.
An approach would be to change the event listener above and insert a new key–value pair that tells the receiver (your app) that the JavaScript sanitizer has ryn, like:
formData.set("is-uri-encoded", "true");
Think of this like a hidden input field that is only inserted when JS runs and your encoding is being used.
Your app can then check whether e.g. the content includes plusses and this key is missing from the request to warn the user, or perform some additional string transformations to sanitize the unprocessed input. If the key is present, the app could treat the input as safe for direct use.
Example HTML File
This is the complete HTML file I’m testing dispatching URL scheme actions with:
<html>
<head>
<meta charset="UTF-8"/>
<title>Form</title>
</head>
<body>
<form action="thearchive-dev://plugin/de.zettelkasten.url-demo/run" method="GET">
<input name="filename" type="text" value="" required/><br>
<textarea cols="30" id="" name="content" rows="10" required></textarea><br>
<input type="submit" value="Create"/>
</form>
<script type="text/javascript">
let form = document.querySelector("form");
form.addEventListener("formdata", (e) => {
const formData = e.formData;
for (let [key, value] of formData.entries()) {
formData.set(key, encodeURIComponent(value));
}
});
</script>
</body>
</html>
That goes well with my URLSchemer Swift package to pattern-match REST-like URL scheme actions, by the way!