# Buggy code
def _search(prefix="latest/"):
    """
    Recursively look up all grains in the metadata server
    """
    ret = {}
    linedata = http.query(os.path.join(HOST, prefix))
    if "body" not in linedata:
        return ret
    for line in linedata["body"].split("\n"):
        if line.endswith("/"):
            ret[line[:-1]] = _search(prefix=os.path.join(prefix, line))
        elif prefix == "latest/":
            # (gtmanfred) The first level should have a forward slash since
            # they have stuff underneath. This will not be doubled up though,
            # because lines ending with a slash are checked first.
            ret[line] = _search(prefix=os.path.join(prefix, line + "/"))
        elif "=" in line:
            key, value = line.split("=")
            ret[value] = _search(prefix=os.path.join(prefix, key))
        else:
            retdata = http.query(os.path.join(HOST, prefix, line)).get("body", None)
            # (gtmanfred) This try except block is slightly faster than
            # checking if the string starts with a curly brace
            try:
                ret[line] = json.loads(retdata)
            except ValueError:
                ret[line] = retdata
    return ret


# Traceback (TypeError)
$ salt-call -l debug grains.items
...
[CRITICAL] Failed to load grains defined in grain file metadata.metadata in function <function metadata at 0x30b0f50>, error:
Traceback (most recent call last):
File "/usr/lib/python2.7/site-packages/salt/loader.py", line 740, in grains
ret = funcs[key]()
File "/usr/lib/python2.7/site-packages/salt/grains/metadata.py", line 77, in metadata
return _search()
File "/usr/lib/python2.7/site-packages/salt/grains/metadata.py", line 61, in _search
ret[line] = _search(prefix=os.path.join(prefix, line + '/'))
File "/usr/lib/python2.7/site-packages/salt/grains/metadata.py", line 70, in _search
ret[line] = json.loads(retdata)
File "/usr/lib64/python2.7/json/__init__.py", line 338, in loads
return _default_decoder.decode(s)
File "/usr/lib64/python2.7/json/decoder.py", line 366, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
TypeError: expected string or buffer

# Fixed code
def _search(prefix="latest/"):
    """
    Recursively look up all grains in the metadata server
    """
    ret = {}
    linedata = http.query(os.path.join(HOST, prefix), headers=True)
    if "body" not in linedata:
        return ret
    if (
        linedata["headers"].get("Content-Type", "text/plain")
        == "application/octet-stream"
    ):
        return linedata["body"]
    for line in linedata["body"].split("\n"):
        if line.endswith("/"):
            ret[line[:-1]] = _search(prefix=os.path.join(prefix, line))
        elif prefix == "latest/":
            # (gtmanfred) The first level should have a forward slash since
            # they have stuff underneath. This will not be doubled up though,
            # because lines ending with a slash are checked first.
            ret[line] = _search(prefix=os.path.join(prefix, line + "/"))
        elif "=" in line:
            key, value = line.split("=")
            ret[value] = _search(prefix=os.path.join(prefix, key))
        else:
            retdata = http.query(os.path.join(HOST, prefix, line)).get("body", None)
            # (gtmanfred) This try except block is slightly faster than
            # checking if the string starts with a curly brace
            try:
                ret[line] = json.loads(retdata)
            except ValueError:
                ret[line] = retdata
    return ret