Repository: aio-libs/yarl

Issue:
Support uppercase characters in host
### Please confirm the following

- [X] I agree to follow the [aio-libs Code of Conduct][CoC]

[CoC]: ../../.github/blob/master/CODE_OF_CONDUCT.md

- [X] I have checked the [current issues][issue search] for duplicates.

[issue search]: ../search?q=is%3Aissue&type=issues

- [X] I understand this is open source software provided for free and that I might not receive a timely response.
- [X] I am positive I am **NOT** reporting a (potential) security
vulnerability, to the best of my knowledge. *(These must be shared by
submitting [this report form][vulnerability report form] instead, if
any hesitation exists.)*

[vulnerability report form]: ../security/advisories/new

- [ ] I am willing to submit a pull request with reporoducers as xfailing test cases or even entire fix. *(Assign this issue to me.)*

### Describe the bug

When using uppercase characters in the hostname they get reported as invalid:

```
>>> URL.build(scheme="http", host="A", port=port, path="/")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "...\yarl\_url.py", line 386, in build
    _host = _encode_host(host, validate_host=True)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...\yarl\_url.py", line 1496, in _encode_host
    raise ValueError(
ValueError: Host 'A' cannot contain 'A' (at position 0)
>>> URL.build(scheme="http", host="a", port=port, path="/")
URL('http://a/')
```

It appears to me, that there needs to be a conversion into lowercase, for example the [`host`-property](https://yarl.aio-libs.org/en/latest/api/#yarl.URL.host) says it gets converted to lowercase (which is the case when using `__init__()`) and the comment at `NOT_REG_NAME` mentions that it only accepts lowercase ASCII values:
```
# this pattern matches anything that is *not* in those classes. and is only used
# on lower-cased ASCII values.
```

When using a non-ASCII string with host, it gets encoded so it seems weird that ASCII uppercase strings aren't "encoded" into lowercase. There is #386, but there it is using `__init__()` which works correctly.

### To Reproduce

1. Install yarl
2. Call `yarl.URL.build(scheme="http", host="A", port=port, path="/")`

### Expected behavior

A valid URL which is identical to the build URL using the lowercase host.

### Logs/tracebacks

```python-traceback
>>> URL.build(scheme="http", host="A", port=port, path="/")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "...\yarl\_url.py", line 386, in build
    _host = _encode_host(host, validate_host=True)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...\yarl\_url.py", line 1496, in _encode_host
    raise ValueError(
ValueError: Host 'A' cannot contain 'A' (at position 0)
>>> URL.build(scheme="http", host="a", port=port, path="/")
URL('http://a/')
```


### Python Version

```console
$ python --version
Python 3.12.2
```


### multidict Version

```console
$ python -m pip show multidict
Name: multidict
Version: 6.1.0
Summary: multidict implementation
Home-page: https://github.com/aio-libs/multidict
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache 2
Location: ..\Lib\site-packages
Requires:
Required-by: yarl
```


### propcache Version

```console
$ python -m pip show propcache
Name: propcache
Version: 0.2.0
Summary: Accelerated property cache
Home-page: https://github.com/aio-libs/propcache
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache-2.0
Location: ..\Lib\site-packages
Requires:
Required-by: yarl
```


### yarl Version

```console
$ python -m pip show yarl
Name: yarl
Version: 1.18.0
Summary: Yet another URL library
Home-page: https://github.com/aio-libs/yarl
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache-2.0
Location: ..\Lib\site-packages
Requires: idna, multidict, propcache
Required-by:
```


### OS

Windows 10

### Additional context

_No response_

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aio-libs/yarl/pull/1442?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) Report
Attention: Patch coverage is `71.42857%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 96.08%. Comparing base [(`65d3aed`)](https://app.codecov.io/gh/aio-libs/yarl/commit/65d3aed2f2dd5ca3494d6de2ccdc150422e09ca2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) to head [(`b33426d`)](https://app.codecov.io/gh/aio-libs/yarl/commit/b33426d21af342ad1397adb2aa00391248dac47f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs).

| [Files with missing lines](https://app.codecov.io/gh/aio-libs/yarl/pull/1442?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | Patch % | Lines |
|---|---|---|
| [tests/test\_url\_build.py](https://app.codecov.io/gh/aio-libs/yarl/pull/1442?src=pr&el=tree&filepath=tests%2Ftest_url_build.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#diff-dGVzdHMvdGVzdF91cmxfYnVpbGQucHk=) | 60.00% | [2 Missing :warning: ](https://app.codecov.io/gh/aio-libs/yarl/pull/1442?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1442      +/-   ##
==========================================
- Coverage   96.11%   96.08%   -0.04%     
==========================================
  Files          31       31              
  Lines        5876     5882       +6     
  Branches      349      349              
==========================================
+ Hits         5648     5652       +4     
- Misses        202      204       +2     
  Partials       26       26              
```

| [Flag](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | Coverage Δ | |
|---|---|---|
| [CI-GHA](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `96.08% <71.42%> (-0.04%)` | :arrow_down: |
| [MyPy](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `49.39% <28.57%> (-0.04%)` | :arrow_down: |
| [OS-Linux](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.55% <100.00%> (+<0.01%)` | :arrow_up: |
| [OS-Windows](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.62% <100.00%> (+<0.01%)` | :arrow_up: |
| [OS-macOS](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.30% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.10.11](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.28% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.10.15](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.51% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.11.10](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.51% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.11.9](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.28% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.12.7](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.51% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.13.0](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.51% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.9.13](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.24% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.9.20](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.47% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-pypy7.3.16](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.53% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-pypy7.3.17](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.55% <100.00%> (+<0.01%)` | :arrow_up: |
| [VM-macos-latest](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.30% <100.00%> (+<0.01%)` | :arrow_up: |
| [VM-ubuntu-latest](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.55% <100.00%> (+<0.01%)` | :arrow_up: |
| [VM-windows-latest](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.62% <100.00%> (+<0.01%)` | :arrow_up: |
| [pytest](https://app.codecov.io/gh/aio-libs/yarl/pull/1442/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.55% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aio-libs/yarl/pull/1442?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs).

codspeed-hq[bot]: ## [CodSpeed Performance Report](https://codspeed.io/aio-libs/yarl/branches/fix_host_validation_upper)

### Merging #1442 will **not alter performance**

<sub>Comparing <code>fix_host_validation_upper</code> (b33426d) with <code>master</code> (65d3aed)</sub>



### Summary

`✅ 85` untouched benchmarks  





Patch:
diff --git a/CHANGES/1442.bugfix.rst b/CHANGES/1442.bugfix.rst
new file mode 100644
index 0000000..a628443
--- /dev/null
+++ b/CHANGES/1442.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed uppercase ASCII hosts being rejected by :meth:`URL.build() <yarl.URL.build>` and :py:meth:`~yarl.URL.with_host` -- by :user:`bdraco`.
diff --git a/CHANGES/954.bugfix.rst b/CHANGES/954.bugfix.rst
new file mode 120000
index 0000000..671f0b1
--- /dev/null
+++ b/CHANGES/954.bugfix.rst
@@ -0,0 +1,1 @@
+1442.bugfix.rst
\ No newline at end of file
diff --git a/yarl/__init__.py b/yarl/__init__.py
index 966e822..583c862 100644
--- a/yarl/__init__.py
+++ b/yarl/__init__.py
@@ -1,7 +1,7 @@
 from ._query import Query, QueryVariable, SimpleQuery
 from ._url import URL, cache_clear, cache_configure, cache_info
 
-__version__ = "1.18.2"
+__version__ = "1.18.3.dev0"
 
 __all__ = (
     "URL",
diff --git a/yarl/_url.py b/yarl/_url.py
index e85e0d6..7118b7a 100644
--- a/yarl/_url.py
+++ b/yarl/_url.py
@@ -1499,6 +1499,7 @@ def _encode_host(host: str, validate_host: bool) -> str:
     if host.isascii():
         # Check for invalid characters explicitly; _idna_encode() does this
         # for non-ascii host names.
+        host = host.lower()
         if validate_host and (invalid := NOT_REG_NAME.search(host)):
             value, pos, extra = invalid.group(), invalid.start(), ""
             if value == "@" or (value == ":" and "@" in host[pos:]):
@@ -1510,7 +1511,7 @@ def _encode_host(host: str, validate_host: bool) -> str:
             raise ValueError(
                 f"Host {host!r} cannot contain {value!r} (at position {pos}){extra}"
             ) from None
-        return host.lower()
+        return host
 
     return _idna_encode(host)
 


Test patch:
diff --git a/tests/test_url_build.py b/tests/test_url_build.py
index 43f1383..b41d269 100644
--- a/tests/test_url_build.py
+++ b/tests/test_url_build.py
@@ -382,3 +382,11 @@ def test_build_with_none_query_string():
 def test_build_with_none_fragment():
     with pytest.raises(TypeError):
         URL.build(scheme="http", host="example.com", fragment=None)
+
+
+def test_build_uppercase_host():
+    u = URL.build(
+        host="UPPER.case",
+        encoded=False,
+    )
+    assert u.host == "upper.case"