· go golang

Using Unescaped Paths in Go

I’m currently writing an API wrapper for Cisco’s ACI and ran into a bug where I was getting a 400 Bad Request and couldn’t understand why. I had a successful http GET with Postman, httpie, and also with the other Go API wrapper for ACI, acigo. As far as I could see, my http request was the same as the one used by these other methods. I left it unsolved last night; I’d had red wine, watched the Election debate, my girlfriend was home from a 14 hour shift in an NHS A&E department and as ever my broken code was unimportant in comparison to her day.

It was only this morning before the kids woke up, armed with coffee that I noticed the only difference between my http request and the others.

My code is fetching a list of VRFs from the APIC server, Cisco ACI’s controller that we interact with via the REST API. To do this we use a GET request to this endpoint:

https://{{APIC_URL}}/api/node/mo/uni/tn-{{TENANT_NAME}}.json?query-target=children&target-subtree-class=fvCtx

As recommended I’ve extracted the request into it’s own Client method:

func (c *Client) newRequest(method string, path string, body interface{}) (*http.Request, error) {
	u := url.URL{Scheme: c.Host.Scheme, Host: c.Host.Host, Path: path}

	var buf io.ReadWriter
	if body != nil {
		buf = new(bytes.Buffer)
		err := json.NewEncoder(buf).Encode(body)
		if err != nil {
			return nil, err
		}
	}

	req, err := http.NewRequest(method, u.String(), buf)
	if err != nil {
		return nil, fmt.Errorf("%s request to %s : %v", method, u.String(), err)
	}
	if c.Cookie != "" {
		req.Header.Set("Cookie", c.Cookie)
	}
	return req, nil
}

After comparing lots of print statements I realised the URL was being escaped to this:

https://{{APIC_URL}}/api/node/mo/uni/tn-{{TENANT_NAME}}.json%3fquery-target=children&target-subtree-class=fvCtx

Notice the ? between json & query is now %3f, its ASCII hex equivalent. We don’t want this escaped as it indicates the query component of the URL:

The query component is indicated by the first question mark (“?”) character and terminated by a number sign (“#”) character or by the end of the URI.

This is why I was getting a 400 Bad Request, the server didn’t understand the escaped URL. So we need to use the raw unescaped path in order to successfully connect with the API endpoint. To do this we use the url package’s Parse method to create our URL rather than do it by hand. Initially I changed this line u := url.URL{Scheme: c.Host.Scheme, Host: c.Host.Host, Path: path} to this:

u, err := url.Parse(path)
if err != nil {
	return nil, err
}
u.Scheme = c.Host.Scheme
u.Host = c.Host.Host

But then reading back through the post mentioned above I noticed something I had overlooked earlier, the ResolveReference method, which resolves the absolute URL for me, and changed my code to this:

rel, err := &url.Parse(path)
if err != nil {
    return nil, err
}
u := c.Host.ResolveReference(rel)

I think I overlooked/deliberately ignored it initially because I didn’t understand what it was doing and so didn’t understand it’s relevance. A better lesson learned through failure. Interestingly the code used in the above mentioned post creates the same error, this code is unsuccessful:

rel := &url.URL{Path: path}
u := c.Host.ResolveReference(rel)

This may not be the best way of dealing with this issue, I’ll learn more as I go. But for now, remember kids, parse your URLs. And vote.

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket