Browser to Xero: Part 1 - CORS
The Xero API doesn't support CORS so it's impossible to access it from a Web browser. Or is it? One solution is to use an existing CORS proxy service. However, for something so simple we could just build our own and avoid the additional dependency. In this post, I'll present a simple Java proxy, followed by another post with example Javascript code to navigate the OAuth 1 labyrinth, and lastly some sample API calls. It's worth pointing out that if you were hoping for an easy plug-and-play project, you'll be out of luck. I'm a firm believer in building your own stuff, if possible. The opposite almost always leads to bloated, slow, brittle, over-engineered rubbish. Enough talk. Let's get started.
Our Options
The generally expected architecture for making Xero calls is via a backend server (see Figure 1) using something like C#, Java, Node.js, etc. That's all fine and dandy if you love MVC, server-side rendering, listening to Vanilla Ice on your Walkman, and watching reruns of MacGyver on your VHS VCR. Fresher approaches are more creative on the client-side so with newer architectures in mind, we have two options. We can define our own backend API and optionally mirror the Xero API (see Figure 2) which is, for the most part, tedious. Or we could build a proxy that knows very little about the API and just transfers calls to and from Xero (see Figure 3).
As mentioned before, there are several free CORS proxy services available. If it's faster for you to get one of those going and you don't mind the additional external dependency (One of them is "down" even as I write this) then knock yourself out and give them a go. I've listed some below in the "Additional Resources" section.
Option 1: This diagram may reveal inherent biases on the part of the author.
Option 2: Not the most efficient idea, but sadly, still very common in the wild.
Option 3: Bingo!
The Proxy
In an unexpected twist, we're going to go with option 3 and talk to Xero through said simple proxy. Since we have no choice but to choose some server-side tech to implement our proxy I'm going to recommend Google's Cloud platform. Why? it was free at the time, and "free" is my favourite price. To get started then, grab yourself Eclipse, learn Java if you need to, create a Google App Engine project, and paste in the following Servlet code (See end of article).
The magic happens mostly in "Do_Get_Post()". Here we extract a few relevant details from the browser's incoming HTTP request, build a corresponding Xero call, make the call, and transfer the data back to the browser. In this simple example we've hard-coded a few details, like the Xero URL and expected JSON result type. CORS access issues are mitigated by both "Set_Content()" and "doOptions()". The former indicates to the browser that the relevant HTTP request is allowed whilst the later, allows for CORS preflight requests to be processed correctly. In usage, this means that for any one Xero API call we wish to make, we need only change the URL host from "api.xero.com" to that of our proxy.
Up Next
Of course there's plenty one could do to improve this code. Instead of hard-coding we could pass the relevant info from the browser or chuck it in a config file. Anyway, once we've got the proxy going the next step is to sort out the requisite OAuth 1 authentication mandated by Xero. This we do in my next post.
Additional Resources
- Dependency Avoidance - https://www.joelonsoftware.com/2001/10/14/in-defense-of-not-invented-here-syndrome/
- In Defense of Not-Invented-Here Syndrome - https://blog.codinghorror.com/dependency-avoidance/
- Xero API Docs - https://developer.xero.com/documentation/libraries/overview
- Does Xero API have CORS headers? - https://community.xero.com/developer/discussion/43807583
- Google Cloud Platform / App Engine - https://cloud.google.com/appengine/
- App Engine Billing - https://cloud.google.com/appengine/quotas#Instances
- CORS Proxy / CORS-Anywhere - https://www.npmjs.com/package/cors-anywhere
- CORS Proxy / CrosssOrigin.me - https://crossorigin.me/
- CORS proxy by HTMLDriven - http://cors-proxy.htmldriven.com/
- HTTP access control (CORS) - https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
The Code
1: public class API_Servlet extends javax.servlet.http.HttpServlet
2: {
3: public void Do_Get_Post(javax.servlet.http.HttpServletRequest req, javax.servlet.http.HttpServletResponse res)
4: throws java.io.IOException
5: {
6: String hdr_auth, fwd_content_type, method, uri;
7: java.net.URL fwdURL;
8: java.net.HttpURLConnection fwdConnection;
9: int fwd_res_code;
10:
11: // extract relevant details from incoming browser Xero call
12: hdr_auth = req.getHeader("Authorization");
13: method = req.getMethod();
14: uri = req.getRequestURI();
15:
16: // build outgoing Xero call
17: fwdURL = new java.net.URL("https://api.xero.com"+uri);
18: fwdConnection = (java.net.HttpURLConnection)fwdURL.openConnection();
19: fwdConnection.setRequestProperty ("Authorization", hdr_auth);
20: fwdConnection.setRequestProperty ("Accept", "application/json");
21: fwdConnection.setRequestMethod(method);
22: fwdConnection.setReadTimeout(0);
23:
24: // call Xero API
25: fwd_res_code = fwdConnection.getResponseCode();
26: fwd_content_type = fwdConnection.getContentType();
27:
28: // pass response to browser
29: Set_Headers(res);
30: res.setContentType(fwd_content_type);
31: res.setStatus(fwd_res_code);
32: Set_Content(res, fwdConnection);
33: }
34:
35: // transfer data from input connection to output response
36: public void Set_Content(javax.servlet.http.HttpServletResponse res, java.net.HttpURLConnection fwdConnection)
37: throws java.io.IOException
38: {
39: int len = 0;
40: byte[] buf = new byte[1024];
41: java.io.InputStream in;
42: java.io.OutputStream out;
43:
44: in = fwdConnection.getInputStream();
45: out = res.getOutputStream();
46: while ((len = in.read(buf)) > 0)
47: {
48: out.write(buf, 0, len);
49: }
50: in.close();
51: }
52:
53: // set CORS friendly headers
54: public void Set_Headers(javax.servlet.http.HttpServletResponse res)
55: {
56: res.addHeader("Access-Control-Allow-Origin", "*");
57: res.addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
58: res.addHeader("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS");
59: }
60:
61: // support CORS preflight http calls
62: public void doOptions(javax.servlet.http.HttpServletRequest req, javax.servlet.http.HttpServletResponse res)
63: throws java.io.IOException
64: {
65: Set_Headers(res);
66: res.setStatus(javax.servlet.http.HttpServletResponse.SC_OK);
67: }
68:
69: public void doGet(javax.servlet.http.HttpServletRequest req, javax.servlet.http.HttpServletResponse res)
70: throws java.io.IOException
71: {
72: Do_Get_Post(req, res);
73: }
74:
75: public void doPost(javax.servlet.http.HttpServletRequest req, javax.servlet.http.HttpServletResponse res)
76: throws java.io.IOException
77: {
78: Do_Get_Post(req, res);
79: }
80: }