Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] HTTPError 415 when providing files using POST #1426

Open
HackXIt opened this issue Jun 27, 2024 · 1 comment
Open

[Bug] HTTPError 415 when providing files using POST #1426

HackXIt opened this issue Jun 27, 2024 · 1 comment

Comments

@HackXIt
Copy link

HackXIt commented Jun 27, 2024

According to psf/requests#1997 when using files in requests, one is not supposed to provide headers, so that the library will take care of the multipart/form-data with the appropriate content-type.

An example: (we wrote a custom extension just to do the XRAY API import)

        # ...
        files = {'file': (os.path.basename(result_file), open(result_file, 'rb'), 'application/xml')}
        return self.post(api_url, files=files, params=params)

Will result in:
requests.exceptions.HTTPError: 415 Client Error: for url: https://[REDACTED]/rest/raven/1.0/api/import/execution/robot?projectKey=TCHCSIPDEV

This is due to the implementation in rest_client.py:

def request(
        self,
        method="GET",
        path="/",
        data=None,
        json=None,
        flags=None,
        params=None,
        headers=None,
        files=None,
        trailing=None,
        absolute=False,
        advanced_mode=False,
    ):
        """

        :param method:
        :param path:
        :param data:
        :param json:
        :param flags:
        :param params:
        :param headers:
        :param files:
        :param trailing: bool - OPTIONAL: Add trailing slash to url
        :param absolute: bool, OPTIONAL: Do not prefix url, url is absolute
        :param advanced_mode: bool, OPTIONAL: Return the raw response
        :return:
        """
        url = self.url_joiner(None if absolute else self.url, path, trailing)
        params_already_in_url = True if "?" in url else False
        if params or flags:
            if params_already_in_url:
                url += "&"
            else:
                url += "?"
        if params:
            url += urlencode(params or {})
        if flags:
            url += ("&" if params or params_already_in_url else "") + "&".join(flags or [])
        json_dump = None
        if files is None:
            data = None if not data else dumps(data)
            json_dump = None if not json else dumps(json)
        self.log_curl_debug(
            method=method,
            url=url,
            headers=headers,
            data=data if data else json_dump,
        )
        headers = headers or self.default_headers # <------- Always provides headers
        response = self._session.request(
            method=method,
            url=url,
            headers=headers, # <------- Should be None when providing files, so that the requests library handles file upload with appropriate headers
            data=data,
            json=json,
            timeout=self.timeout,
            verify=self.verify_ssl,
            files=files,
            proxies=self.proxies,
            cert=self.cert,
        )
        response.encoding = "utf-8"

        log.debug("HTTP: %s %s -> %s %s", method, path, response.status_code, response.reason)
        log.debug("HTTP: Response text -> %s", response.text)
        if self.advanced_mode or advanced_mode:
            return response

        self.raise_for_status(response)
        return response

If we change this to:

        headers = headers or self.default_headers if files is None else None

Then the request will work, as the multipart/form-data is appropriately handled by the requests library.

This is necessary in APIs like XRAY, where one can import result files, which are not JSON.
But it should also be applicable for JSON files.

@HackXIt
Copy link
Author

HackXIt commented Jun 27, 2024

Provided files should also modify the behavior of:

def log_curl_debug(self, method, url, data=None, headers=None, level=logging.DEBUG):
        """

        :param method:
        :param url:
        :param data:
        :param headers:
        :param level:
        :return:
        """
        headers = headers or self.default_headers
        message = "curl --silent -X {method} -H {headers} {data} '{url}'".format(
            method=method,
            headers=" -H ".join(["'{0}: {1}'".format(key, value) for key, value in headers.items()]),
            data="" if not data else "--data '{0}'".format(dumps(data)),

Which currently does not consider files at all, but I understand that this a debugging functionality and is not applicable in many other cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant