JSONP has always been one of the most poorly explained concepts in all of web development. This is likely due to its confusing name and overall sketchy background. Prior to the adoption of the Cross-Origin Resource Sharing (CORS) standard, JSONP was the only option to get a JSON response from a server of a different origin.
After sending a request to a server of a different origin that doesn’t support CORS, the following error would be thrown:
Upon seeing this, many people would Google it just to find out that JSONP would be needed to bypass the same-origin policy. Then jQuery, ubiquitous back in the day, would swoop in with its convenient JSONP implementation baked right into the core library so that we could get it working by switching just one parameter. Many people never understood that what changed completely was the underlying mechanism of sending the request.
$.ajax({ url: 'http://twitter.com/status/user_timeline/padraicb.json?count=10', dataType: 'jsonp', success: function onSuccess() { } });
In order to understand what went on behind the scenes, let’s take a look at what JSONP really is.
JSON with Padding — JSONP for short — is a technique that allows developers to bypass the same-origin policy enforced by browsers by using the <script>
element’s nature. The policy disallows reading any responses sent by websites whose origins are different from the one currently used. Incidentally, the policy allows sending a request, but not reading one.
A website’s origin consists of three parts. First, there’s the URI scheme (i.e., https://
), then the host name (i.e., logrocket.com
), and, finally, the port (i.e., 443
). Websites like http://logrocket.com
and https://logrocket.com
have two different origins due to the URI Scheme difference.
If you wish to learn more about this policy, look no further.
Let’s assume that we are on localhost:8000
and we send a request to a server providing a JSON API.
https://www.server.com/api/person/1
The response may look like this:
{ "firstName": "Maciej", "lastName": "Cieslar" }
But due to the aforementioned policy, the request would be blocked because the origins of the website and the server differ.
Instead of sending the request ourselves, the <script>
element can be used, to which the policy doesn’t apply — it can load and execute JavaScript from a source of foreign origin. This way, a website located on https://logrocket.com
can load the Google Maps library from its provider located under a different origin (i.e., CDN).
By providing the API’s endpoint URL to the <script>
’s src
attribute, the <script>
would fetch the response and execute it inside the browser context.
<script src="https://www.server.com/api/person/1" async="true"></script>
The problem, though, is that the <script>
element automatically parses and executes the returned code. In this case, the returned code would be the JSON snippet shown above. The JSON would be parsed as JavaScript code and, thus, throw an error because it is not a valid JavaScript.
A fully working JavaScript code has to be returned for it to be parsed and executed correctly by the <script>
. The JSON code would work just fine had we assigned it to a variable or passed it as an argument to a function — after all, the JSON format is just a JavaScript object.
So instead of returning a pure JSON response, the server can return a JavaScript code. In the returned code, a function is wrapped around the JSON object. The function name has to be passed by the client since the code is going to be executed in the browser. The function name is provided in the query parameter called callback
.
After providing the callback’s name in the query, we create a function in the global (window
) context, which will be called once the response is parsed and executed.
https://www.server.com/api/person/1?callback=callbackName
callbackName({ "firstName": "Maciej", "lastName": "Cieslar" })
Which is the same as:
window.callbackName({ "firstName": "Maciej", "lastName": "Cieslar" })
The code is executed in the browser’s context. The function will be executed from inside the code downloaded in <script>
in the global scope.
In order for JSONP to work, both the client and the server have to support it. While there’s no standard name for the parameter that defines the name of the function, the client will usually send it in the query parameter named callback
.
Let’s create a function called jsonp
that will send the request in the JSONP fashion.
let jsonpID = 0; function jsonp(url, timeout = 7500) { const head = document.querySelector('head'); jsonpID += 1; return new Promise((resolve, reject) => { let script = document.createElement('script'); const callbackName = `jsonpCallback${jsonpID}`; script.src = encodeURI(`${url}?callback=${callbackName}`); script.async = true; const timeoutId = window.setTimeout(() => { cleanUp(); return reject(new Error('Timeout')); }, timeout); window[callbackName] = data => { cleanUp(); return resolve(data); }; script.addEventListener('error', error => { cleanUp(); return reject(error); }); function cleanUp() { window[callbackName] = undefined; head.removeChild(script); window.clearTimeout(timeoutId); script = null; } head.appendChild(script); }); }
As you can see, there’s a shared variable called jsonpID
— it will be used to make sure that each request has its own unique function name.
First, we save the reference to the <head>
object inside a variable called head
. Then we increment the jsonpID
to make sure the function name is unique. Inside the callback provided to the returned promise, we create a <script>
element and the callbackName
consisting of the string jsonpCallback
concatenated with the unique ID.
Then, we set the src
attribute of the <script>
element to the provided URL. Inside the query, we set the callback parameter to equal callbackName
. Note that this simplified implementation doesn’t support URLs that have predefined query parameters, so it wouldn’t work for something like https://logrocket.com/?param=true
, because we would append ?
at the end once again.
We also set the async
attribute to true
in order for the script to be non-blocking.
There are three possible outcomes of the request:
window[callbackName]
, which resolves the promise with the result (JSON)<script>
element throws an error and we reject the promiseconst timeoutId = window.setTimeout(() => { cleanUp(); return reject(new Error('Timeout')); }, timeout); window[callbackName] = data => { cleanUp(); return resolve(data); }; script.addEventListener('error', error => { cleanUp(); return reject(error); });
The callback has to be registered on the window
object for it to be available from inside the created <script>
context. Executing a function called callback()
in the global scope is equivalent to calling window.callback()
.
By abstracting the cleanup process in the cleanUp
function, the three callbacks — timeout, success, and error listener — look exactly the same. The only difference is whether they resolve or reject the promise.
function cleanUp() { window[callbackName] = undefined; head.removeChild(script); window.clearTimeout(timeoutId); script = null; }
The cleanUp
function is an abstraction of what needs to be done in order to clean up after the request. The function first removes the callback registered on the window, which is called upon successful response. Then it removes the <script>
element from <head>
and clears the timeout. Also, just to be sure, it sets the script
reference to null
so that it is garbage-collected.
Finally, we append the <script>
element to <head>
in order to fire the request. <script>
will send the request automatically once it is appended.
Here’s the example of the usage:
jsonp('https://gist.github.com/maciejcieslar/1c1f79d5778af4c2ee17927de769cea3.json') .then(console.log) .catch(console.error);
Here’s a live example.
By understanding the underlying mechanism of JSONP, you probably won’t gain much in terms of directly applicable web skills, but it’s always interesting to see how people’s ingenuity can bypass even the strictest policies.
JSONP is a relic of the past and shouldn’t be used due to numerous limitations (e.g., being able to send GET requests only) and many security concerns (e.g., the server can respond with whatever JavaScript code it wants — not necessarily the one we expect — which then has access to everything in the context of the window, including localStorage
and cookies
). Read more here.
Instead, we should rely on the CORS mechanism to provide safe cross-origin requests.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.