# Buggy 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 line.endswith(("dynamic", "meta-data")):
            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] = salt.utils.json.loads(retdata)
            except ValueError:
                ret[line] = retdata
    return ret


# Traceback (TypeError)
2018-04-05 02:52:35,470 [salt.loader      :763 ][CRITICAL][2496] Failed to load grains defined in grain file metadata.metadata in function <function metadata at 0x0000000001BA9198>, error:
Traceback (most recent call last):
File "c:\salt\bin\lib\site-packages\salt\loader.py", line 756, in grains
ret = funcs[key]()
File "c:\salt\var\cache\salt\minion\extmods\grains\metadata.py", line 79, in metadata
return _search()
File "c:\salt\var\cache\salt\minion\extmods\grains\metadata.py", line 63, in _search
ret[line] = _search(prefix=os.path.join(prefix, line + '/'))
File "c:\salt\var\cache\salt\minion\extmods\grains\metadata.py", line 58, in _search
ret[line[:-1]] = _search(prefix=os.path.join(prefix, line))
File "c:\salt\var\cache\salt\minion\extmods\grains\metadata.py", line 66, in _search
ret[value] = _search(prefix=os.path.join(prefix, key))
File "c:\salt\var\cache\salt\minion\extmods\grains\metadata.py", line 72, in _search
ret[line] = json.loads(retdata)
File "c:\salt\bin\lib\json\__init__.py", line 339, in loads
return _default_decoder.decode(s)
File "c:\salt\bin\lib\json\decoder.py", line 364, 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
    body = salt.utils.stringutils.to_unicode(linedata["body"])
    if (
        linedata["headers"].get("Content-Type", "text/plain")
        == "application/octet-stream"
    ):
        return body
    for line in 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 line.endswith(("dynamic", "meta-data")):
            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
            if isinstance(retdata, six.binary_type):
                try:
                    ret[line] = salt.utils.json.loads(
                        salt.utils.stringutils.to_unicode(retdata)
                    )
                except ValueError:
                    ret[line] = salt.utils.stringutils.to_unicode(retdata)
            else:
                ret[line] = retdata
    return salt.utils.data.decode(ret)