I know, JSONP is basically a hack to circumvent dealing with CORS inside browsers. It executes whatever Javascript is sent back, so it is also unsafe. But there still are many public APIs that are only consumable using JSONP (or you're forced to build a proxy server). It doesn't even necessarily have to be a public API. Simply if you're stuck with an API that you have to consume that has strict CORS headers, it most likely will support JSONP.
Proposal
Add two functions to elm-http
for dealing with JSONP so we can leverage the usage of Tasks
. One is a Native
function, the other is an Elm function that accepts a decoder.
jsonp : String -> String -> String -> Task x String
jsonp =
Native.Send.jsonp
jsonpGet : Json.Decoder value -> String -> String -> Task Error value
jsonpGet decoder url callbackParam =
let
decode s =
Json.decodeString decoder s
|> Task.fromResult
|> Task.mapError Http.UnexpectedPayload
in
randomCallbackName
`Task.andThen` jsonp url callbackParam
`Task.andThen` decode
As you can see, there is no err
handling for JSONP. This is because there is no way of knowing whether it will fail. You load the contents into a HTML <script>
tag and let your browser take the wheel. I am sure that 99% of the time, JSONP will be used to parse JSON. So if there is any wrong output being generated in the <script>
tag, the decode should fail with Http.UnexpectedPayload
.
JSONP circumvents CORS by loading javascript wrapped in a callback into a <script>
tag. <script>
tags can load any URL (barring HTTP/HTTPS discrepancies). The important thing is the callback. There could be multiple JSONP requests going on parallel, so we need to generate a global callback function that is unique for every JSONP Task
. We use the Random
module for this. The function randomCallbackName
could look like this:
import Random
import Time
randomCallbackName : Task x String
randomCallbackName =
let
generator =
Random.int 100 Random.maxInt
in
Time.now
|> Task.map (Random.step generator Random.initialSeed)
|> Task.map (fst >> toString >> (++) "callback")
We're left with a random callback name that starts with "callback"
and has a random number added to it. This random callback name will be used in the JS side. This is what the JS side could look like.
var _evancz$elm_http$Native_Http = function() {
function jsonp(url, callbackParam, callbackName)
{
return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) {
window[callbackName] = function(content)
{
callback(_elm_lang$core$Native_Scheduler.succeed(JSON.stringify(content)));
};
var scriptTag = createScript(url, callbackParam, callbackName);
document.head.appendChild(scriptTag);
document.head.removeChild(scriptTag);
});
}
function createScript(url, callbackParam, callbackName)
{
var s = document.createElement('script');
s.type = 'text/javascript';
if (url.indexOf('?') >= 0)
{
s.src = url + '&' + callbackParam + '=' + callbackName;
}
else {
s.src = url + '?' + callbackParam + '=' + callbackName;
}
return s;
}
return {
jsonp: F3(jsonp)
};
}();
We create a global function using window[callbackName]
. This function receives the content from the JSONP call. To avoid doing anything unsafe, we JSON.stringify
the callback contents and pass it to the Scheduler with succeed
. The script tag gets created with the HTTP call URL, and it is appended and removed from the document <head>
.
Discussion points
- JSONP is a hack to circumvent CORS. Do we want to support something like this in Elm?
- It loads any Javascript returned from the server into a script tag, so it could be unsafe. In this implementation, I use
JSON.stringify
on the callback contents.
- This is a rough mockup. I'm already using this code in a project of mine, and it's working great. Maybe some naming could be changed?