Skip to content

fix(machinery): limit allowed URLs#18684

Open
nijel wants to merge 1 commit intoWeblateOrg:mainfrom
nijel:url-validate
Open

fix(machinery): limit allowed URLs#18684
nijel wants to merge 1 commit intoWeblateOrg:mainfrom
nijel:url-validate

Conversation

@nijel
Copy link
Member

@nijel nijel commented Mar 27, 2026

Allow only public URLs by default to reduce risk of SSRF.

@nijel nijel added this to the 5.17 milestone Mar 27, 2026
@nijel nijel requested review from amCap1712 and Copilot March 27, 2026 12:59
@nijel nijel self-assigned this Mar 27, 2026
@nijel nijel requested a review from AliceVisek as a code owner March 27, 2026 12:59
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens project-level machine translation configuration and runtime HTTP fetching to reduce SSRF risk by blocking private-network targets (unless explicitly allowed) and by restricting what remote error details can be surfaced.

Changes:

  • Introduces outbound URL/hostname validation helpers and wires them into machinery form/API validation (with an allowlist via ALLOWED_MACHINERY_DOMAINS).
  • Adds runtime URL validation (DNS + peer IP checks) into http_request, including redirect handling, to prevent private-target access and DNS rebinding.
  • Adjusts machinery error-detail handling to avoid exposing untrusted provider response bodies; adds trusted_error_hosts for known providers and expands tests/docs.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
weblate/utils/outbound.py New outbound URL/hostname/IP validation helpers (config-time + runtime).
weblate/utils/requests.py Adds URL-validation mode to http_request with redirect + peer-IP checks.
weblate/utils/tests/test_requests.py Adds unit tests covering URL-validation behavior (private targets, redirects, proxy cases).
weblate/utils/validators.py Adds machinery-specific URL/hostname validators using outbound guards + allowlist.
weblate/utils/tests/test_validators.py Tests for the new machinery validators and allowlist behavior.
weblate/utils/models.py Adds ALLOWED_MACHINERY_DOMAINS setting default.
weblate/machinery/forms.py Validates endpoint fields and propagates allow_private_targets into machinery validation.
weblate/machinery/views.py Passes allow_private_targets to forms; disables private targets for project-level edits.
weblate/machinery/models.py Threads allow_private_targets through service configuration validation.
weblate/api/views.py Enforces allow_private_targets=False for project machinery settings API.
weblate/api/tests.py Adds API test ensuring private project targets are rejected.
weblate/machinery/base.py Adds trusted-host logic, runtime URL validation, and revised error-detail extraction.
weblate/machinery/openai.py Adds runtime base URL validation and trusted hosts for OpenAI/Azure OpenAI.
weblate/machinery/deepl.py Declares DeepL trusted error hosts.
weblate/machinery/libretranslate.py Declares LibreTranslate trusted error hosts.
weblate/machinery/anthropic.py Declares Anthropic trusted error hosts.
weblate/machinery/microsoft.py Improves Microsoft host detection for regional endpoints in error handling.
weblate/machinery/tests.py Adds validation/error-detail tests and runtime URL validation coverage.
docs/admin/config.rst Documents ALLOWED_MACHINERY_DOMAINS.
docs/changes.rst Notes SSRF hardening and error-detail behavior change in release notes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9154c9d7fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@codecov
Copy link

codecov bot commented Mar 27, 2026

❌ 10 Tests Failed:

Tests completed Failed Passed Skipped
5930 10 5920 705
View the top 3 failed test(s) by shortest run time
weblate.screenshots.tests.ViewTest::test_invalid_image_url_size
Stack Traces | 0.2s run time
self = <weblate.screenshots.tests.ViewTest testMethod=test_invalid_image_url_size>

    @responses.activate
    def test_invalid_image_url_size(self) -> None:
        self.make_manager()
        # Mock a too big image
        responses.add(
            responses.GET,
            "https://example.com/big-image.png",
            content_type="image/png",
            body=b"x" * (settings.ALLOWED_ASSET_SIZE + 1),
        )
>       response = self.do_upload(
            image="", image_url="https://example.com/big-image.png"
        )

weblate/screenshots/tests.py:422: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
weblate/screenshots/tests.py:48: in do_upload
    return self.client.post(
.venv/lib/python3.14.../django/test/client.py:1153: in post
    response = super().post(
.venv/lib/python3.14.../django/test/client.py:499: in post
    return self.generic(
.venv/lib/python3.14.../django/test/client.py:671: in generic
    return self.request(**r)
           ^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/test/client.py:1087: in request
    self.check_exception(response)
.venv/lib/python3.14.../django/test/client.py:802: in check_exception
    raise exc_value
.venv/lib/python3.14.../core/handlers/exception.py:55: in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../core/handlers/base.py:198: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:106: in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:145: in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/views.py:233: in post
    if self._add_form.is_valid():
       ^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:206: in is_valid
    return self.is_bound and not self.errors
                                 ^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:201: in errors
    self.full_clean()
.venv/lib/python3.14.../django/forms/forms.py:338: in full_clean
    self._clean_form()
.venv/lib/python3.14.../django/forms/forms.py:354: in _clean_form
    cleaned_data = self.clean()
                   ^^^^^^^^^^^^
weblate/screenshots/forms.py:176: in clean
    return self.clean_images(cleaned_data or {})
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:42: in clean_images
    cleaned_data["image"] = self.download_image(image_url)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:52: in download_image
    with asset_request("get", url, stream=True) as response:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/contextlib.py:141: in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

method = 'get', url = 'https://example.com/big-image.png', headers = None
timeout = 5, raise_for_status = True, max_redirects = 5
kwargs = {'stream': True}

    @contextmanager
    def asset_request(
        method: str,
        url: str,
        *,
        headers: dict[str, str] | None = None,
        timeout: float = 5,
        raise_for_status: bool = True,
        max_redirects: int = 5,
        **kwargs,
    ) -> Generator[Response, None, None]:
        """Fetch an asset while validating each redirect target before following it."""
>       response = _request_with_redirects(
            method,
            url,
            headers=headers,
            timeout=timeout,
            stream=False,
            max_redirects=max_redirects,
            validate_url=validate_asset_url,
            **kwargs,
        )
E       TypeError: weblate.utils.requests._request_with_redirects() got multiple values for keyword argument 'stream'

weblate/utils/requests.py:227: TypeError
weblate.screenshots.tests.ViewTest::test_invalid_image_url_content
Stack Traces | 0.243s run time
self = <weblate.screenshots.tests.ViewTest testMethod=test_invalid_image_url_content>

    @responses.activate
    def test_invalid_image_url_content(self) -> None:
        self.make_manager()
        # Mock a non-image content
        responses.add(
            responses.GET,
            "https://example.com/invalid-image.png",
            content_type="image/png",
            body=b"x",
        )
>       response = self.do_upload(
            image="", image_url="https://example.com/invalid-image.png"
        )

weblate/screenshots/tests.py:437: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
weblate/screenshots/tests.py:48: in do_upload
    return self.client.post(
.venv/lib/python3.14.../django/test/client.py:1153: in post
    response = super().post(
.venv/lib/python3.14.../django/test/client.py:499: in post
    return self.generic(
.venv/lib/python3.14.../django/test/client.py:671: in generic
    return self.request(**r)
           ^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/test/client.py:1087: in request
    self.check_exception(response)
.venv/lib/python3.14.../django/test/client.py:802: in check_exception
    raise exc_value
.venv/lib/python3.14.../core/handlers/exception.py:55: in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../core/handlers/base.py:198: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:106: in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:145: in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/views.py:233: in post
    if self._add_form.is_valid():
       ^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:206: in is_valid
    return self.is_bound and not self.errors
                                 ^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:201: in errors
    self.full_clean()
.venv/lib/python3.14.../django/forms/forms.py:338: in full_clean
    self._clean_form()
.venv/lib/python3.14.../django/forms/forms.py:354: in _clean_form
    cleaned_data = self.clean()
                   ^^^^^^^^^^^^
weblate/screenshots/forms.py:176: in clean
    return self.clean_images(cleaned_data or {})
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:42: in clean_images
    cleaned_data["image"] = self.download_image(image_url)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:52: in download_image
    with asset_request("get", url, stream=True) as response:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/contextlib.py:141: in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

method = 'get', url = 'https://example.com/invalid-image.png', headers = None
timeout = 5, raise_for_status = True, max_redirects = 5
kwargs = {'stream': True}

    @contextmanager
    def asset_request(
        method: str,
        url: str,
        *,
        headers: dict[str, str] | None = None,
        timeout: float = 5,
        raise_for_status: bool = True,
        max_redirects: int = 5,
        **kwargs,
    ) -> Generator[Response, None, None]:
        """Fetch an asset while validating each redirect target before following it."""
>       response = _request_with_redirects(
            method,
            url,
            headers=headers,
            timeout=timeout,
            stream=False,
            max_redirects=max_redirects,
            validate_url=validate_asset_url,
            **kwargs,
        )
E       TypeError: weblate.utils.requests._request_with_redirects() got multiple values for keyword argument 'stream'

weblate/utils/requests.py:227: TypeError
weblate.screenshots.tests.ViewTest::test_disallowed_image_url_redirect_domain
Stack Traces | 0.279s run time
self = <weblate.screenshots.tests.ViewTest testMethod=test_disallowed_image_url_redirect_domain>

    @responses.activate
    @override_settings(ALLOWED_ASSET_DOMAINS=[".allowed.com"])
    def test_disallowed_image_url_redirect_domain(self) -> None:
        """Reject redirects leaving the allowed asset domains."""
        self.make_manager()
        responses.add(
            responses.GET,
            "https://images.allowed.com/redirect-image.png",
            status=302,
            headers={"Location": "https://proof.example.com/final-image.png"},
        )
        responses.add(
            responses.GET,
            "https://proof.example.com/final-image.png",
            content_type="image/png",
            body=Path(TEST_SCREENSHOT).read_bytes(),
        )
    
>       response = self.do_upload(
            image="", image_url="https://images.allowed.com/redirect-image.png"
        )

weblate/screenshots/tests.py:470: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
weblate/screenshots/tests.py:48: in do_upload
    return self.client.post(
.venv/lib/python3.14.../django/test/client.py:1153: in post
    response = super().post(
.venv/lib/python3.14.../django/test/client.py:499: in post
    return self.generic(
.venv/lib/python3.14.../django/test/client.py:671: in generic
    return self.request(**r)
           ^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/test/client.py:1087: in request
    self.check_exception(response)
.venv/lib/python3.14.../django/test/client.py:802: in check_exception
    raise exc_value
.venv/lib/python3.14.../core/handlers/exception.py:55: in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../core/handlers/base.py:198: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:106: in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:145: in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/views.py:233: in post
    if self._add_form.is_valid():
       ^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:206: in is_valid
    return self.is_bound and not self.errors
                                 ^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:201: in errors
    self.full_clean()
.venv/lib/python3.14.../django/forms/forms.py:338: in full_clean
    self._clean_form()
.venv/lib/python3.14.../django/forms/forms.py:354: in _clean_form
    cleaned_data = self.clean()
                   ^^^^^^^^^^^^
weblate/screenshots/forms.py:176: in clean
    return self.clean_images(cleaned_data or {})
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:42: in clean_images
    cleaned_data["image"] = self.download_image(image_url)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:52: in download_image
    with asset_request("get", url, stream=True) as response:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/contextlib.py:141: in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

method = 'get', url = 'https://images.allowed.com/redirect-image.png'
headers = None, timeout = 5, raise_for_status = True, max_redirects = 5
kwargs = {'stream': True}

    @contextmanager
    def asset_request(
        method: str,
        url: str,
        *,
        headers: dict[str, str] | None = None,
        timeout: float = 5,
        raise_for_status: bool = True,
        max_redirects: int = 5,
        **kwargs,
    ) -> Generator[Response, None, None]:
        """Fetch an asset while validating each redirect target before following it."""
>       response = _request_with_redirects(
            method,
            url,
            headers=headers,
            timeout=timeout,
            stream=False,
            max_redirects=max_redirects,
            validate_url=validate_asset_url,
            **kwargs,
        )
E       TypeError: weblate.utils.requests._request_with_redirects() got multiple values for keyword argument 'stream'

weblate/utils/requests.py:227: TypeError
weblate.screenshots.tests.ViewTest::test_invalid_image_url_content_type
Stack Traces | 0.285s run time
self = <weblate.screenshots.tests.ViewTest testMethod=test_invalid_image_url_content_type>

    @responses.activate
    def test_invalid_image_url_content_type(self) -> None:
        self.make_manager()
        # Mock a non-image content type
        responses.add(
            responses.GET,
            "https://example.com/not-an-image.png",
            content_type="text/html",
        )
>       response = self.do_upload(
            image="", image_url="https://example.com/not-an-image.png"
        )

weblate/screenshots/tests.py:407: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
weblate/screenshots/tests.py:48: in do_upload
    return self.client.post(
.venv/lib/python3.14.../django/test/client.py:1153: in post
    response = super().post(
.venv/lib/python3.14.../django/test/client.py:499: in post
    return self.generic(
.venv/lib/python3.14.../django/test/client.py:671: in generic
    return self.request(**r)
           ^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/test/client.py:1087: in request
    self.check_exception(response)
.venv/lib/python3.14.../django/test/client.py:802: in check_exception
    raise exc_value
.venv/lib/python3.14.../core/handlers/exception.py:55: in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../core/handlers/base.py:198: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:106: in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:145: in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/views.py:233: in post
    if self._add_form.is_valid():
       ^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:206: in is_valid
    return self.is_bound and not self.errors
                                 ^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:201: in errors
    self.full_clean()
.venv/lib/python3.14.../django/forms/forms.py:338: in full_clean
    self._clean_form()
.venv/lib/python3.14.../django/forms/forms.py:354: in _clean_form
    cleaned_data = self.clean()
                   ^^^^^^^^^^^^
weblate/screenshots/forms.py:176: in clean
    return self.clean_images(cleaned_data or {})
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:42: in clean_images
    cleaned_data["image"] = self.download_image(image_url)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:52: in download_image
    with asset_request("get", url, stream=True) as response:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/contextlib.py:141: in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

method = 'get', url = 'https://example.com/not-an-image.png', headers = None
timeout = 5, raise_for_status = True, max_redirects = 5
kwargs = {'stream': True}

    @contextmanager
    def asset_request(
        method: str,
        url: str,
        *,
        headers: dict[str, str] | None = None,
        timeout: float = 5,
        raise_for_status: bool = True,
        max_redirects: int = 5,
        **kwargs,
    ) -> Generator[Response, None, None]:
        """Fetch an asset while validating each redirect target before following it."""
>       response = _request_with_redirects(
            method,
            url,
            headers=headers,
            timeout=timeout,
            stream=False,
            max_redirects=max_redirects,
            validate_url=validate_asset_url,
            **kwargs,
        )
E       TypeError: weblate.utils.requests._request_with_redirects() got multiple values for keyword argument 'stream'

weblate/utils/requests.py:227: TypeError
weblate.screenshots.tests.ViewTest::test_disallowed_image_url_domain
Stack Traces | 0.33s run time
self = <weblate.screenshots.tests.ViewTest testMethod=test_disallowed_image_url_domain>

    @responses.activate
    @override_settings(ALLOWED_ASSET_DOMAINS=[".allowed.com"])
    def test_disallowed_image_url_domain(self) -> None:
        """Test validation when image URL domain is not allowed."""
        self.make_manager()
>       response = self.do_upload(
            image="", image_url="https://example.com/not-allowed-image.png"
        )

weblate/screenshots/tests.py:447: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
weblate/screenshots/tests.py:48: in do_upload
    return self.client.post(
.venv/lib/python3.14.../django/test/client.py:1153: in post
    response = super().post(
.venv/lib/python3.14.../django/test/client.py:499: in post
    return self.generic(
.venv/lib/python3.14.../django/test/client.py:671: in generic
    return self.request(**r)
           ^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/test/client.py:1087: in request
    self.check_exception(response)
.venv/lib/python3.14.../django/test/client.py:802: in check_exception
    raise exc_value
.venv/lib/python3.14.../core/handlers/exception.py:55: in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../core/handlers/base.py:198: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:106: in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:145: in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/views.py:233: in post
    if self._add_form.is_valid():
       ^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:206: in is_valid
    return self.is_bound and not self.errors
                                 ^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:201: in errors
    self.full_clean()
.venv/lib/python3.14.../django/forms/forms.py:338: in full_clean
    self._clean_form()
.venv/lib/python3.14.../django/forms/forms.py:354: in _clean_form
    cleaned_data = self.clean()
                   ^^^^^^^^^^^^
weblate/screenshots/forms.py:176: in clean
    return self.clean_images(cleaned_data or {})
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:42: in clean_images
    cleaned_data["image"] = self.download_image(image_url)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:52: in download_image
    with asset_request("get", url, stream=True) as response:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/contextlib.py:141: in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

method = 'get', url = 'https://example.com/not-allowed-image.png'
headers = None, timeout = 5, raise_for_status = True, max_redirects = 5
kwargs = {'stream': True}

    @contextmanager
    def asset_request(
        method: str,
        url: str,
        *,
        headers: dict[str, str] | None = None,
        timeout: float = 5,
        raise_for_status: bool = True,
        max_redirects: int = 5,
        **kwargs,
    ) -> Generator[Response, None, None]:
        """Fetch an asset while validating each redirect target before following it."""
>       response = _request_with_redirects(
            method,
            url,
            headers=headers,
            timeout=timeout,
            stream=False,
            max_redirects=max_redirects,
            validate_url=validate_asset_url,
            **kwargs,
        )
E       TypeError: weblate.utils.requests._request_with_redirects() got multiple values for keyword argument 'stream'

weblate/utils/requests.py:227: TypeError
weblate.screenshots.tests.ViewTest::test_image_url_download_failure
Stack Traces | 0.361s run time
self = <weblate.screenshots.tests.ViewTest testMethod=test_image_url_download_failure>

    @responses.activate
    def test_image_url_download_failure(self) -> None:
        """Test handling of image download failures."""
        self.make_manager()
        responses.add(
            responses.GET,
            "https://example.com/missing-image.png",
            content_type="text/html",
            status=301,
        )
        responses.add(
            responses.GET,
            "https://example.com/broken-image.png",
            body=requests.RequestException("Network error"),
        )
>       response = self.do_upload(
            image="", image_url="https://example.com/missing-image.png"
        )

weblate/screenshots/tests.py:369: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
weblate/screenshots/tests.py:48: in do_upload
    return self.client.post(
.venv/lib/python3.14.../django/test/client.py:1153: in post
    response = super().post(
.venv/lib/python3.14.../django/test/client.py:499: in post
    return self.generic(
.venv/lib/python3.14.../django/test/client.py:671: in generic
    return self.request(**r)
           ^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/test/client.py:1087: in request
    self.check_exception(response)
.venv/lib/python3.14.../django/test/client.py:802: in check_exception
    raise exc_value
.venv/lib/python3.14.../core/handlers/exception.py:55: in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../core/handlers/base.py:198: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:106: in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:145: in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/views.py:233: in post
    if self._add_form.is_valid():
       ^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:206: in is_valid
    return self.is_bound and not self.errors
                                 ^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:201: in errors
    self.full_clean()
.venv/lib/python3.14.../django/forms/forms.py:338: in full_clean
    self._clean_form()
.venv/lib/python3.14.../django/forms/forms.py:354: in _clean_form
    cleaned_data = self.clean()
                   ^^^^^^^^^^^^
weblate/screenshots/forms.py:176: in clean
    return self.clean_images(cleaned_data or {})
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:42: in clean_images
    cleaned_data["image"] = self.download_image(image_url)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:52: in download_image
    with asset_request("get", url, stream=True) as response:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/contextlib.py:141: in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

method = 'get', url = 'https://example.com/missing-image.png', headers = None
timeout = 5, raise_for_status = True, max_redirects = 5
kwargs = {'stream': True}

    @contextmanager
    def asset_request(
        method: str,
        url: str,
        *,
        headers: dict[str, str] | None = None,
        timeout: float = 5,
        raise_for_status: bool = True,
        max_redirects: int = 5,
        **kwargs,
    ) -> Generator[Response, None, None]:
        """Fetch an asset while validating each redirect target before following it."""
>       response = _request_with_redirects(
            method,
            url,
            headers=headers,
            timeout=timeout,
            stream=False,
            max_redirects=max_redirects,
            validate_url=validate_asset_url,
            **kwargs,
        )
E       TypeError: weblate.utils.requests._request_with_redirects() got multiple values for keyword argument 'stream'

weblate/utils/requests.py:227: TypeError
weblate.screenshots.tests.ViewTest::test_upload_with_image_url
Stack Traces | 0.362s run time
self = <weblate.screenshots.tests.ViewTest testMethod=test_upload_with_image_url>

    @responses.activate
    def test_upload_with_image_url(self) -> None:
        data = Path(TEST_SCREENSHOT).read_bytes()
        responses.add(
            responses.GET,
            "https://example.com/test-image.png",
            content_type="image/png",
            body=data,
        )
    
        self.make_manager()
>       response = self.do_upload(
            image="", image_url="https://example.com/test-image.png"
        )

weblate/screenshots/tests.py:320: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
weblate/screenshots/tests.py:48: in do_upload
    return self.client.post(
.venv/lib/python3.14.../django/test/client.py:1153: in post
    response = super().post(
.venv/lib/python3.14.../django/test/client.py:499: in post
    return self.generic(
.venv/lib/python3.14.../django/test/client.py:671: in generic
    return self.request(**r)
           ^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/test/client.py:1087: in request
    self.check_exception(response)
.venv/lib/python3.14.../django/test/client.py:802: in check_exception
    raise exc_value
.venv/lib/python3.14.../core/handlers/exception.py:55: in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../core/handlers/base.py:198: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:106: in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:145: in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/views.py:233: in post
    if self._add_form.is_valid():
       ^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:206: in is_valid
    return self.is_bound and not self.errors
                                 ^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:201: in errors
    self.full_clean()
.venv/lib/python3.14.../django/forms/forms.py:338: in full_clean
    self._clean_form()
.venv/lib/python3.14.../django/forms/forms.py:354: in _clean_form
    cleaned_data = self.clean()
                   ^^^^^^^^^^^^
weblate/screenshots/forms.py:176: in clean
    return self.clean_images(cleaned_data or {})
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:42: in clean_images
    cleaned_data["image"] = self.download_image(image_url)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:52: in download_image
    with asset_request("get", url, stream=True) as response:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/contextlib.py:141: in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

method = 'get', url = 'https://example.com/test-image.png', headers = None
timeout = 5, raise_for_status = True, max_redirects = 5
kwargs = {'stream': True}

    @contextmanager
    def asset_request(
        method: str,
        url: str,
        *,
        headers: dict[str, str] | None = None,
        timeout: float = 5,
        raise_for_status: bool = True,
        max_redirects: int = 5,
        **kwargs,
    ) -> Generator[Response, None, None]:
        """Fetch an asset while validating each redirect target before following it."""
>       response = _request_with_redirects(
            method,
            url,
            headers=headers,
            timeout=timeout,
            stream=False,
            max_redirects=max_redirects,
            validate_url=validate_asset_url,
            **kwargs,
        )
E       TypeError: weblate.utils.requests._request_with_redirects() got multiple values for keyword argument 'stream'

weblate/utils/requests.py:227: TypeError
weblate.screenshots.tests.ViewTest::test_edit_with_image_url
Stack Traces | 0.39s run time
self = <weblate.screenshots.tests.ViewTest testMethod=test_edit_with_image_url>

    @responses.activate
    def test_edit_with_image_url(self) -> None:
        self.make_manager()
        self.do_upload()
        screenshot = Screenshot.objects.all()[0]
        old_name = screenshot.image.name
        old_filename = screenshot.image.file.name
    
        data = Path(TEST_SCREENSHOT).read_bytes()
        responses.add(
            responses.GET,
            "https://example.com/test-image.png",
            content_type="image/png",
            body=data,
        )
    
>       self.client.post(
            screenshot.get_absolute_url(),
            {
                "image_url": "https://example.com/test-image.png",
                "name": "Updated screenshot",
            },
            follow=True,
        )

weblate/screenshots/tests.py:342: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.14.../django/test/client.py:1153: in post
    response = super().post(
.venv/lib/python3.14.../django/test/client.py:499: in post
    return self.generic(
.venv/lib/python3.14.../django/test/client.py:671: in generic
    return self.request(**r)
           ^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/test/client.py:1087: in request
    self.check_exception(response)
.venv/lib/python3.14.../django/test/client.py:802: in check_exception
    raise exc_value
.venv/lib/python3.14.../core/handlers/exception.py:55: in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../core/handlers/base.py:198: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:106: in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:145: in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/views.py:302: in post
    if self._edit_form.is_valid():
       ^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:206: in is_valid
    return self.is_bound and not self.errors
                                 ^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:201: in errors
    self.full_clean()
.venv/lib/python3.14.../django/forms/forms.py:338: in full_clean
    self._clean_form()
.venv/lib/python3.14.../django/forms/forms.py:354: in _clean_form
    cleaned_data = self.clean()
                   ^^^^^^^^^^^^
weblate/screenshots/forms.py:127: in clean
    return self.clean_images(cleaned_data or {}, edit=True)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:42: in clean_images
    cleaned_data["image"] = self.download_image(image_url)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:52: in download_image
    with asset_request("get", url, stream=True) as response:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/contextlib.py:141: in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

method = 'get', url = 'https://example.com/test-image.png', headers = None
timeout = 5, raise_for_status = True, max_redirects = 5
kwargs = {'stream': True}

    @contextmanager
    def asset_request(
        method: str,
        url: str,
        *,
        headers: dict[str, str] | None = None,
        timeout: float = 5,
        raise_for_status: bool = True,
        max_redirects: int = 5,
        **kwargs,
    ) -> Generator[Response, None, None]:
        """Fetch an asset while validating each redirect target before following it."""
>       response = _request_with_redirects(
            method,
            url,
            headers=headers,
            timeout=timeout,
            stream=False,
            max_redirects=max_redirects,
            validate_url=validate_asset_url,
            **kwargs,
        )
E       TypeError: weblate.utils.requests._request_with_redirects() got multiple values for keyword argument 'stream'

weblate/utils/requests.py:227: TypeError
weblate.api.tests.ProjectAPITest::test_install_machinery
Stack Traces | 2.09s run time
self = <weblate.api.tests.ProjectAPITest testMethod=test_install_machinery>

    @responses.activate
    def test_install_machinery(self) -> None:
        """Test the machinery settings API endpoint for various scenarios."""
        from weblate.machinery.tests import AlibabaTranslationTest, DeepLTranslationTest
    
        # unauthenticated get
        self.do_request(
            "api:project-machinery-settings",
            self.project_kwargs,
            method="get",
            code=403,
            authenticated=True,
            superuser=False,
        )
    
        # unauthenticated post
        self.do_request(
            "api:project-machinery-settings",
            self.project_kwargs,
            method="post",
            code=403,
            authenticated=True,
            superuser=False,
            request={"service": "weblate"},
        )
    
        # missing service
        response = self.do_request(
            "api:project-machinery-settings",
            self.project_kwargs,
            method="post",
            code=400,
            superuser=True,
            request={},
        )
    
        # unknown service
        response = self.do_request(
            "api:project-machinery-settings",
            self.project_kwargs,
            method="post",
            code=400,
            superuser=True,
            request={"service": "unknown"},
        )
    
        # create configuration: no form
        response = self.do_request(
            "api:project-machinery-settings",
            self.project_kwargs,
            method="post",
            code=201,
            superuser=True,
            request={"service": "weblate"},
        )
    
        # missing required field
        response = self.do_request(
            "api:project-machinery-settings",
            self.project_kwargs,
            method="post",
            code=400,
            superuser=True,
            request={
                "service": "deepl",
                "configuration": {"wrong": ""},
            },
            format="json",
        )
    
        # invalid field with multipart
        response = self.do_request(
            "api:project-machinery-settings",
            self.project_kwargs,
            method="post",
            code=400,
            superuser=True,
            request={
                "service": "deepl",
                "configuration": "{malformed_json",
            },
        )
    
        # create configuration: valid form with multipart
        DeepLTranslationTest.mock_response()
>       response = self.do_request(
            "api:project-machinery-settings",
            self.project_kwargs,
            method="post",
            code=201,
            superuser=True,
            request={
                "service": "deepl",
                "configuration": '{"key": "x", "url": "https://api.deepl.com/v2/"}',
            },
        )

weblate/api/tests.py:3314: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
weblate/api/tests.py:145: in do_request
    self.assertEqual(
E   AssertionError: 400 != 201 : Unexpected status code 400: b'{"type":"validation_error","errors":[{"code":"invalid","detail":"Could not fetch supported languages: [\'Could not verify URL domain.\']","attr":"configuration"}]}'
weblate.screenshots.tests.ViewTest::test_allowed_image_url_redirect_domain
Stack Traces | 3.71s run time
self = <weblate.screenshots.tests.ViewTest testMethod=test_allowed_image_url_redirect_domain>

    @responses.activate
    @override_settings(ALLOWED_ASSET_DOMAINS=[".allowed.com"])
    def test_allowed_image_url_redirect_domain(self) -> None:
        """Allow redirects that stay within the allowed asset domains."""
        self.make_manager()
        responses.add(
            responses.GET,
            "https://images.allowed.com/redirect-image.png",
            status=302,
            headers={"Location": "https://cdn.allowed.com/final-image.png"},
        )
        responses.add(
            responses.GET,
            "https://cdn.allowed.com/final-image.png",
            content_type="image/png",
            body=Path(TEST_SCREENSHOT).read_bytes(),
        )
    
>       response = self.do_upload(
            image="", image_url="https://images.allowed.com/redirect-image.png"
        )

weblate/screenshots/tests.py:495: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
weblate/screenshots/tests.py:48: in do_upload
    return self.client.post(
.venv/lib/python3.14.../django/test/client.py:1153: in post
    response = super().post(
.venv/lib/python3.14.../django/test/client.py:499: in post
    return self.generic(
.venv/lib/python3.14.../django/test/client.py:671: in generic
    return self.request(**r)
           ^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/test/client.py:1087: in request
    self.check_exception(response)
.venv/lib/python3.14.../django/test/client.py:802: in check_exception
    raise exc_value
.venv/lib/python3.14.../core/handlers/exception.py:55: in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../core/handlers/base.py:198: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:106: in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:145: in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/views.py:233: in post
    if self._add_form.is_valid():
       ^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:206: in is_valid
    return self.is_bound and not self.errors
                                 ^^^^^^^^^^^
.venv/lib/python3.14.../django/forms/forms.py:201: in errors
    self.full_clean()
.venv/lib/python3.14.../django/forms/forms.py:338: in full_clean
    self._clean_form()
.venv/lib/python3.14.../django/forms/forms.py:354: in _clean_form
    cleaned_data = self.clean()
                   ^^^^^^^^^^^^
weblate/screenshots/forms.py:176: in clean
    return self.clean_images(cleaned_data or {})
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:42: in clean_images
    cleaned_data["image"] = self.download_image(image_url)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/screenshots/forms.py:52: in download_image
    with asset_request("get", url, stream=True) as response:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../homebrew/Cellar/python@3.14/3.14.3_1/Frameworks/Python.framework/Versions/3.14/lib/python3.14/contextlib.py:141: in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

method = 'get', url = 'https://images.allowed.com/redirect-image.png'
headers = None, timeout = 5, raise_for_status = True, max_redirects = 5
kwargs = {'stream': True}

    @contextmanager
    def asset_request(
        method: str,
        url: str,
        *,
        headers: dict[str, str] | None = None,
        timeout: float = 5,
        raise_for_status: bool = True,
        max_redirects: int = 5,
        **kwargs,
    ) -> Generator[Response, None, None]:
        """Fetch an asset while validating each redirect target before following it."""
>       response = _request_with_redirects(
            method,
            url,
            headers=headers,
            timeout=timeout,
            stream=False,
            max_redirects=max_redirects,
            validate_url=validate_asset_url,
            **kwargs,
        )
E       TypeError: weblate.utils.requests._request_with_redirects() got multiple values for keyword argument 'stream'

weblate/utils/requests.py:227: TypeError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 040c2e5080

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Allow only public URLs by default to reduce risk of SSRF.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +39 to +50
def validate_request_url(url: str, *, allow_private_targets: bool = True) -> None:
with requests.Session() as session:
request_settings = session.merge_environment_settings(
url,
{},
False,
None,
None,
)
used_proxy = select_proxy(url, request_settings["proxies"]) is not None
validator = validate_outbound_url if used_proxy else validate_runtime_url
validator(url, allow_private_targets=allow_private_targets)
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_request_url() (and the validate_proxied_url lambda in http_request()) calls validate_outbound_url() without passing allowed_domains. As a result, settings.ALLOWED_MACHINERY_DOMAINS allowlisted hostnames are still rejected at runtime when a proxy is used (e.g., single-label hosts like ollama), even though configuration-time validation allows them. Consider extending validate_request_url()/http_request() to accept and pass through allowed_domains (and have machinery pass settings.ALLOWED_MACHINERY_DOMAINS), and add a regression test for an allowlisted proxied hostname.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +104
The allowlist only affects configuration-time validation for project-managed
machinery endpoints. For direct connections, runtime checks still reject
destinations that resolve to private or otherwise non-public addresses. When an
HTTP(S) proxy is used, runtime validation falls back to hostname validation and
does not perform the same local DNS or peer-IP checks.
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This docs section says the allowlist "only affects configuration-time validation", but the code also uses ALLOWED_MACHINERY_DOMAINS to decide whether remote error details can be displayed for project-level machinery (trusted host check in BatchMachineTranslation). Please update the wording to reflect that the allowlist affects both endpoint validation and which provider error messages/bodies may be surfaced.

Suggested change
The allowlist only affects configuration-time validation for project-managed
machinery endpoints. For direct connections, runtime checks still reject
destinations that resolve to private or otherwise non-public addresses. When an
HTTP(S) proxy is used, runtime validation falls back to hostname validation and
does not perform the same local DNS or peer-IP checks.
The allowlist is used both for configuration-time validation of project-managed
machinery endpoints and as a trusted-host check to decide whether remote
provider error messages or response bodies may be surfaced for project-level
machinery. For direct connections, runtime checks still reject destinations that
resolve to private or otherwise non-public addresses. When an HTTP(S) proxy is
used, runtime validation falls back to hostname validation and does not perform
the same local DNS or peer-IP checks.

Copilot uses AI. Check for mistakes.
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 10ccdfaeb7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +60 to +64
lowered = hostname.lower()
if lowered == "localhost" or lowered.endswith(LOCAL_HOST_SUFFIXES):
raise ValidationError(gettext("URL domain is not allowed."))
if "." not in hostname:
raise ValidationError(gettext("URL domain is not allowed."))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize hostname aliases before trust checks

validate_untrusted_hostname only blocks exact localhost/.localhost suffixes and canonical IP literals, so aliases like http://localhost./ or shorthand IPv4 forms like http://127.1/ pass this check. In proxied flows (_request_with_redirects uses validate_outbound_url when a proxy is selected), no DNS/peer-IP validation runs afterward, so these hostnames can be resolved by the proxy to loopback/private targets and bypass the new SSRF guard. Canonicalizing hostnames (strip trailing dot, normalize/resolve numeric host forms) before the localhost/private checks would close this gap.

Useful? React with 👍 / 👎.

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

Successfully merging this pull request may close these issues.

2 participants