# Buggy code
def get_url(
    self,
    url,
    dest,
    makedirs=False,
    saltenv="base",
    no_cache=False,
    cachedir=None,
    source_hash=None,
):
    """
    Get a single file from a URL.
    """
    url_data = urlparse(url)
    url_scheme = url_data.scheme
    url_path = os.path.join(url_data.netloc, url_data.path).rstrip(os.sep)

    # If dest is a directory, rewrite dest with filename
    if dest is not None and (os.path.isdir(dest) or dest.endswith(("/", "\\"))):
        if url_data.query or len(url_data.path) > 1 and not url_data.path.endswith("/"):
            strpath = url.split("/")[-1]
        else:
            strpath = "index.html"

        if salt.utils.platform.is_windows():
            strpath = salt.utils.path.sanitize_win_path(strpath)

        dest = os.path.join(dest, strpath)

    if url_scheme and url_scheme.lower() in string.ascii_lowercase:
        url_path = ":".join((url_scheme, url_path))
        url_scheme = "file"

    if url_scheme in ("file", ""):
        # Local filesystem
        if not os.path.isabs(url_path):
            raise CommandExecutionError("Path '{0}' is not absolute".format(url_path))
        if dest is None:
            with salt.utils.files.fopen(url_path, "rb") as fp_:
                data = fp_.read()
            return data
        return url_path

    if url_scheme == "salt":
        result = self.get_file(url, dest, makedirs, saltenv, cachedir=cachedir)
        if result and dest is None:
            with salt.utils.files.fopen(result, "rb") as fp_:
                data = fp_.read()
            return data
        return result

    if dest:
        destdir = os.path.dirname(dest)
        if not os.path.isdir(destdir):
            if makedirs:
                os.makedirs(destdir)
            else:
                return ""
    elif not no_cache:
        dest = self._extrn_path(url, saltenv, cachedir=cachedir)
        if source_hash is not None:
            try:
                source_hash = source_hash.split("=")[-1]
                form = salt.utils.files.HASHES_REVMAP[len(source_hash)]
                if salt.utils.hashutils.get_hash(dest, form) == source_hash:
                    log.debug(
                        "Cached copy of %s (%s) matches source_hash %s, "
                        "skipping download",
                        url,
                        dest,
                        source_hash,
                    )
                    return dest
            except (AttributeError, KeyError, IOError, OSError):
                pass
        destdir = os.path.dirname(dest)
        if not os.path.isdir(destdir):
            os.makedirs(destdir)

    if url_data.scheme == "s3":
        try:

            def s3_opt(key, default=None):
                """
                Get value of s3.<key> from Minion config or from Pillar
                """
                if "s3." + key in self.opts:
                    return self.opts["s3." + key]
                try:
                    return self.opts["pillar"]["s3"][key]
                except (KeyError, TypeError):
                    return default

            self.utils["s3.query"](
                method="GET",
                bucket=url_data.netloc,
                path=url_data.path[1:],
                return_bin=False,
                local_file=dest,
                action=None,
                key=s3_opt("key"),
                keyid=s3_opt("keyid"),
                service_url=s3_opt("service_url"),
                verify_ssl=s3_opt("verify_ssl", True),
                location=s3_opt("location"),
                path_style=s3_opt("path_style", False),
                https_enable=s3_opt("https_enable", True),
            )
            return dest
        except Exception as exc:  # pylint: disable=broad-except
            raise MinionError(
                "Could not fetch from {0}. Exception: {1}".format(url, exc)
            )
    if url_data.scheme == "ftp":
        try:
            ftp = ftplib.FTP()
            ftp.connect(url_data.hostname, url_data.port)
            ftp.login(url_data.username, url_data.password)
            remote_file_path = url_data.path.lstrip("/")
            with salt.utils.files.fopen(dest, "wb") as fp_:
                ftp.retrbinary("RETR {0}".format(remote_file_path), fp_.write)
            ftp.quit()
            return dest
        except Exception as exc:  # pylint: disable=broad-except
            raise MinionError(
                "Could not retrieve {0} from FTP server. Exception: {1}".format(
                    url, exc
                )
            )

    if url_data.scheme == "swift":
        try:

            def swift_opt(key, default):
                """
                Get value of <key> from Minion config or from Pillar
                """
                if key in self.opts:
                    return self.opts[key]
                try:
                    return self.opts["pillar"][key]
                except (KeyError, TypeError):
                    return default

            swift_conn = SaltSwift(
                swift_opt("keystone.user", None),
                swift_opt("keystone.tenant", None),
                swift_opt("keystone.auth_url", None),
                swift_opt("keystone.password", None),
            )

            swift_conn.get_object(url_data.netloc, url_data.path[1:], dest)
            return dest
        except Exception:  # pylint: disable=broad-except
            raise MinionError("Could not fetch from {0}".format(url))

    get_kwargs = {}
    if url_data.username is not None and url_data.scheme in ("http", "https"):
        netloc = url_data.netloc
        at_sign_pos = netloc.rfind("@")
        if at_sign_pos != -1:
            netloc = netloc[at_sign_pos + 1 :]
        fixed_url = urlunparse(
            (
                url_data.scheme,
                netloc,
                url_data.path,
                url_data.params,
                url_data.query,
                url_data.fragment,
            )
        )
        get_kwargs["auth"] = (url_data.username, url_data.password)
    else:
        fixed_url = url

    destfp = None
    try:
        # Tornado calls streaming_callback on redirect response bodies.
        # But we need streaming to support fetching large files (> RAM
        # avail). Here we are working around this by disabling recording
        # the body for redirections. The issue is fixed in Tornado 4.3.0
        # so on_header callback could be removed when we'll deprecate
        # Tornado<4.3.0. See #27093 and #30431 for details.

        # Use list here to make it writable inside the on_header callback.
        # Simple bool doesn't work here: on_header creates a new local
        # variable instead. This could be avoided in Py3 with 'nonlocal'
        # statement. There is no Py2 alternative for this.
        #
        # write_body[0] is used by the on_chunk callback to tell it whether
        #   or not we need to write the body of the request to disk. For
        #   30x redirects we set this to False because we don't want to
        #   write the contents to disk, as we will need to wait until we
        #   get to the redirected URL.
        #
        # write_body[1] will contain a tornado.httputil.HTTPHeaders
        #   instance that we will use to parse each header line. We
        #   initialize this to False, and after we parse the status line we
        #   will replace it with the HTTPHeaders instance. If/when we have
        #   found the encoding used in the request, we set this value to
        #   False to signify that we are done parsing.
        #
        # write_body[2] is where the encoding will be stored
        write_body = [None, False, None]

        def on_header(hdr):
            if write_body[1] is not False and write_body[2] is None:
                if not hdr.strip() and "Content-Type" not in write_body[1]:
                    # If write_body[0] is True, then we are not following a
                    # redirect (initial response was a 200 OK). So there is
                    # no need to reset write_body[0].
                    if write_body[0] is not True:
                        # We are following a redirect, so we need to reset
                        # write_body[0] so that we properly follow it.
                        write_body[0] = None
                    # We don't need the HTTPHeaders object anymore
                    write_body[1] = False
                    return
                # Try to find out what content type encoding is used if
                # this is a text file
                write_body[1].parse_line(hdr)  # pylint: disable=no-member
                if "Content-Type" in write_body[1]:
                    content_type = write_body[1].get("Content-Type")  # pylint: disable=no-member
                    if not content_type.startswith("text"):
                        write_body[1] = write_body[2] = False
                    else:
                        encoding = "utf-8"
                        fields = content_type.split(";")
                        for field in fields:
                            if "encoding" in field:
                                encoding = field.split("encoding=")[-1]
                        write_body[2] = encoding
                        # We have found our encoding. Stop processing headers.
                        write_body[1] = False

                    # If write_body[0] is False, this means that this
                    # header is a 30x redirect, so we need to reset
                    # write_body[0] to None so that we parse the HTTP
                    # status code from the redirect target. Additionally,
                    # we need to reset write_body[2] so that we inspect the
                    # headers for the Content-Type of the URL we're
                    # following.
                    if write_body[0] is write_body[1] is False:
                        write_body[0] = write_body[2] = None

            # Check the status line of the HTTP request
            if write_body[0] is None:
                try:
                    hdr = parse_response_start_line(hdr)
                except HTTPInputError:
                    # Not the first line, do nothing
                    return
                write_body[0] = hdr.code not in [301, 302, 303, 307]
                write_body[1] = HTTPHeaders()

        if no_cache:
            result = []

            def on_chunk(chunk):
                if write_body[0]:
                    if write_body[2]:
                        chunk = chunk.decode(write_body[2])
                    result.append(chunk)
        else:
            dest_tmp = "{0}.part".format(dest)
            # We need an open filehandle to use in the on_chunk callback,
            # that's why we're not using a with clause here.
            destfp = salt.utils.files.fopen(dest_tmp, "wb")  # pylint: disable=resource-leakage

            def on_chunk(chunk):
                if write_body[0]:
                    destfp.write(chunk)

        query = salt.utils.http.query(
            fixed_url,
            stream=True,
            streaming_callback=on_chunk,
            header_callback=on_header,
            username=url_data.username,
            password=url_data.password,
            opts=self.opts,
            **get_kwargs,
        )
        if "handle" not in query:
            raise MinionError("Error: {0} reading {1}".format(query["error"], url))
        if no_cache:
            if write_body[2]:
                return "".join(result)
            return b"".join(result)
        else:
            destfp.close()
            destfp = None
            salt.utils.files.rename(dest_tmp, dest)
            return dest
    except HTTPError as exc:
        raise MinionError(
            "HTTP error {0} reading {1}: {3}".format(
                exc.code,
                url,
                *BaseHTTPServer.BaseHTTPRequestHandler.responses[exc.code],
            )
        )
    except URLError as exc:
        raise MinionError("Error reading {0}: {1}".format(url, exc.reason))
    finally:
        if destfp is not None:
            destfp.close()


# Traceback (TypeError)
salt '*' cp.get_url ftp://ftp.freebsd.org/pub/FreeBSD/releases/amd64/amd64/12.0-RELEASE/MANIFEST /tmp/MANIFEST
[WARNING ] /usr/local/lib/python3.6/site-packages/salt/transport/ipc.py:292: DeprecationWarning: encoding is deprecated, Use raw=False instead.
self.unpacker = msgpack.Unpacker(encoding=encoding)

[WARNING ] /usr/local/lib/python3.6/site-packages/salt/payload.py:149: DeprecationWarning: encoding is deprecated, Use raw=False instead.
ret = msgpack.loads(msg, use_list=True, ext_hook=ext_type_decoder, encoding=encoding)

testminion:
The minion function caused an exception: Traceback (most recent call last):
File "/usr/local/lib/python3.6/site-packages/salt/fileclient.py", line 540, in get_url
ftp.connect(url_data.hostname, url_data.port)
File "/usr/local/lib/python3.6/ftplib.py", line 145, in connect
if port > 0:
TypeError: '>' not supported between instances of 'NoneType' and 'int'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/usr/local/lib/python3.6/site-packages/salt/minion.py", line 1660, in _thread_return
return_data = minion_instance.executors[fname](opts, data, func, args, kwargs)
File "/usr/local/lib/python3.6/site-packages/salt/executors/direct_call.py", line 12, in execute
return func(*args, **kwargs)
File "/usr/local/lib/python3.6/site-packages/salt/modules/cp.py", line 418, in get_url
path, dest, makedirs, saltenv, source_hash=source_hash)
File "/usr/local/lib/python3.6/site-packages/salt/fileclient.py", line 548, in get_url
raise MinionError('Could not retrieve {0} from FTP server. Exception: {1}'.format(url, exc))
salt.exceptions.MinionError: Could not retrieve ftp://ftp.freebsd.org/pub/FreeBSD/releases/amd64/amd64/12.0-RELEASE/MANIFEST from FTP server. Exception: '>' not supported between instances of 'NoneType' and 'int'
ERROR: Minions returned with non-zero exit code

# Fixed code
def get_url(
    self,
    url,
    dest,
    makedirs=False,
    saltenv="base",
    no_cache=False,
    cachedir=None,
    source_hash=None,
):
    """
    Get a single file from a URL.
    """
    url_data = urlparse(url)
    url_scheme = url_data.scheme
    url_path = os.path.join(url_data.netloc, url_data.path).rstrip(os.sep)

    # If dest is a directory, rewrite dest with filename
    if dest is not None and (os.path.isdir(dest) or dest.endswith(("/", "\\"))):
        if url_data.query or len(url_data.path) > 1 and not url_data.path.endswith("/"):
            strpath = url.split("/")[-1]
        else:
            strpath = "index.html"

        if salt.utils.platform.is_windows():
            strpath = salt.utils.path.sanitize_win_path(strpath)

        dest = os.path.join(dest, strpath)

    if url_scheme and url_scheme.lower() in string.ascii_lowercase:
        url_path = ":".join((url_scheme, url_path))
        url_scheme = "file"

    if url_scheme in ("file", ""):
        # Local filesystem
        if not os.path.isabs(url_path):
            raise CommandExecutionError("Path '{0}' is not absolute".format(url_path))
        if dest is None:
            with salt.utils.files.fopen(url_path, "rb") as fp_:
                data = fp_.read()
            return data
        return url_path

    if url_scheme == "salt":
        result = self.get_file(url, dest, makedirs, saltenv, cachedir=cachedir)
        if result and dest is None:
            with salt.utils.files.fopen(result, "rb") as fp_:
                data = fp_.read()
            return data
        return result

    if dest:
        destdir = os.path.dirname(dest)
        if not os.path.isdir(destdir):
            if makedirs:
                os.makedirs(destdir)
            else:
                return ""
    elif not no_cache:
        dest = self._extrn_path(url, saltenv, cachedir=cachedir)
        if source_hash is not None:
            try:
                source_hash = source_hash.split("=")[-1]
                form = salt.utils.files.HASHES_REVMAP[len(source_hash)]
                if salt.utils.hashutils.get_hash(dest, form) == source_hash:
                    log.debug(
                        "Cached copy of %s (%s) matches source_hash %s, "
                        "skipping download",
                        url,
                        dest,
                        source_hash,
                    )
                    return dest
            except (AttributeError, KeyError, IOError, OSError):
                pass
        destdir = os.path.dirname(dest)
        if not os.path.isdir(destdir):
            os.makedirs(destdir)

    if url_data.scheme == "s3":
        try:

            def s3_opt(key, default=None):
                """
                Get value of s3.<key> from Minion config or from Pillar
                """
                if "s3." + key in self.opts:
                    return self.opts["s3." + key]
                try:
                    return self.opts["pillar"]["s3"][key]
                except (KeyError, TypeError):
                    return default

            self.utils["s3.query"](
                method="GET",
                bucket=url_data.netloc,
                path=url_data.path[1:],
                return_bin=False,
                local_file=dest,
                action=None,
                key=s3_opt("key"),
                keyid=s3_opt("keyid"),
                service_url=s3_opt("service_url"),
                verify_ssl=s3_opt("verify_ssl", True),
                location=s3_opt("location"),
                path_style=s3_opt("path_style", False),
                https_enable=s3_opt("https_enable", True),
            )
            return dest
        except Exception as exc:  # pylint: disable=broad-except
            raise MinionError(
                "Could not fetch from {0}. Exception: {1}".format(url, exc)
            )
    if url_data.scheme == "ftp":
        try:
            ftp = ftplib.FTP()
            ftp_port = url_data.port
            if not ftp_port:
                ftp_port = 21
            ftp.connect(url_data.hostname, ftp_port)
            ftp.login(url_data.username, url_data.password)
            remote_file_path = url_data.path.lstrip("/")
            with salt.utils.files.fopen(dest, "wb") as fp_:
                ftp.retrbinary("RETR {0}".format(remote_file_path), fp_.write)
            ftp.quit()
            return dest
        except Exception as exc:  # pylint: disable=broad-except
            raise MinionError(
                "Could not retrieve {0} from FTP server. Exception: {1}".format(
                    url, exc
                )
            )

    if url_data.scheme == "swift":
        try:

            def swift_opt(key, default):
                """
                Get value of <key> from Minion config or from Pillar
                """
                if key in self.opts:
                    return self.opts[key]
                try:
                    return self.opts["pillar"][key]
                except (KeyError, TypeError):
                    return default

            swift_conn = SaltSwift(
                swift_opt("keystone.user", None),
                swift_opt("keystone.tenant", None),
                swift_opt("keystone.auth_url", None),
                swift_opt("keystone.password", None),
            )

            swift_conn.get_object(url_data.netloc, url_data.path[1:], dest)
            return dest
        except Exception:  # pylint: disable=broad-except
            raise MinionError("Could not fetch from {0}".format(url))

    get_kwargs = {}
    if url_data.username is not None and url_data.scheme in ("http", "https"):
        netloc = url_data.netloc
        at_sign_pos = netloc.rfind("@")
        if at_sign_pos != -1:
            netloc = netloc[at_sign_pos + 1 :]
        fixed_url = urlunparse(
            (
                url_data.scheme,
                netloc,
                url_data.path,
                url_data.params,
                url_data.query,
                url_data.fragment,
            )
        )
        get_kwargs["auth"] = (url_data.username, url_data.password)
    else:
        fixed_url = url

    destfp = None
    try:
        # Tornado calls streaming_callback on redirect response bodies.
        # But we need streaming to support fetching large files (> RAM
        # avail). Here we are working around this by disabling recording
        # the body for redirections. The issue is fixed in Tornado 4.3.0
        # so on_header callback could be removed when we'll deprecate
        # Tornado<4.3.0. See #27093 and #30431 for details.

        # Use list here to make it writable inside the on_header callback.
        # Simple bool doesn't work here: on_header creates a new local
        # variable instead. This could be avoided in Py3 with 'nonlocal'
        # statement. There is no Py2 alternative for this.
        #
        # write_body[0] is used by the on_chunk callback to tell it whether
        #   or not we need to write the body of the request to disk. For
        #   30x redirects we set this to False because we don't want to
        #   write the contents to disk, as we will need to wait until we
        #   get to the redirected URL.
        #
        # write_body[1] will contain a tornado.httputil.HTTPHeaders
        #   instance that we will use to parse each header line. We
        #   initialize this to False, and after we parse the status line we
        #   will replace it with the HTTPHeaders instance. If/when we have
        #   found the encoding used in the request, we set this value to
        #   False to signify that we are done parsing.
        #
        # write_body[2] is where the encoding will be stored
        write_body = [None, False, None]

        def on_header(hdr):
            if write_body[1] is not False and write_body[2] is None:
                if not hdr.strip() and "Content-Type" not in write_body[1]:
                    # If write_body[0] is True, then we are not following a
                    # redirect (initial response was a 200 OK). So there is
                    # no need to reset write_body[0].
                    if write_body[0] is not True:
                        # We are following a redirect, so we need to reset
                        # write_body[0] so that we properly follow it.
                        write_body[0] = None
                    # We don't need the HTTPHeaders object anymore
                    write_body[1] = False
                    return
                # Try to find out what content type encoding is used if
                # this is a text file
                write_body[1].parse_line(hdr)  # pylint: disable=no-member
                if "Content-Type" in write_body[1]:
                    content_type = write_body[1].get("Content-Type")  # pylint: disable=no-member
                    if not content_type.startswith("text"):
                        write_body[1] = write_body[2] = False
                    else:
                        encoding = "utf-8"
                        fields = content_type.split(";")
                        for field in fields:
                            if "encoding" in field:
                                encoding = field.split("encoding=")[-1]
                        write_body[2] = encoding
                        # We have found our encoding. Stop processing headers.
                        write_body[1] = False

                    # If write_body[0] is False, this means that this
                    # header is a 30x redirect, so we need to reset
                    # write_body[0] to None so that we parse the HTTP
                    # status code from the redirect target. Additionally,
                    # we need to reset write_body[2] so that we inspect the
                    # headers for the Content-Type of the URL we're
                    # following.
                    if write_body[0] is write_body[1] is False:
                        write_body[0] = write_body[2] = None

            # Check the status line of the HTTP request
            if write_body[0] is None:
                try:
                    hdr = parse_response_start_line(hdr)
                except HTTPInputError:
                    # Not the first line, do nothing
                    return
                write_body[0] = hdr.code not in [301, 302, 303, 307]
                write_body[1] = HTTPHeaders()

        if no_cache:
            result = []

            def on_chunk(chunk):
                if write_body[0]:
                    if write_body[2]:
                        chunk = chunk.decode(write_body[2])
                    result.append(chunk)
        else:
            dest_tmp = "{0}.part".format(dest)
            # We need an open filehandle to use in the on_chunk callback,
            # that's why we're not using a with clause here.
            destfp = salt.utils.files.fopen(dest_tmp, "wb")  # pylint: disable=resource-leakage

            def on_chunk(chunk):
                if write_body[0]:
                    destfp.write(chunk)

        query = salt.utils.http.query(
            fixed_url,
            stream=True,
            streaming_callback=on_chunk,
            header_callback=on_header,
            username=url_data.username,
            password=url_data.password,
            opts=self.opts,
            **get_kwargs,
        )
        if "handle" not in query:
            raise MinionError("Error: {0} reading {1}".format(query["error"], url))
        if no_cache:
            if write_body[2]:
                return "".join(result)
            return b"".join(result)
        else:
            destfp.close()
            destfp = None
            salt.utils.files.rename(dest_tmp, dest)
            return dest
    except HTTPError as exc:
        raise MinionError(
            "HTTP error {0} reading {1}: {3}".format(
                exc.code,
                url,
                *BaseHTTPServer.BaseHTTPRequestHandler.responses[exc.code],
            )
        )
    except URLError as exc:
        raise MinionError("Error reading {0}: {1}".format(url, exc.reason))
    finally:
        if destfp is not None:
            destfp.close()