Repository: aio-libs/yarl

Issue:
[injection] updated URL.join empty path change with 1.9.10
### 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

1.9.10 changed the behavior of URL.join which ended up breaking several calls of ours.  

If you constructed an base URL (same behavior across both versions): 
```python
>>> import yarl
>>> yarl.__version__
'1.9.10'
>>> base = yarl.URL.build(scheme='https', host='localhost', port=443)
>>> base.path
'/'
>>> base._val.path
''
```

(note difference in what is stated as the path and what is stored as the path).

In 1.9.9 if you did a join like so you get the following:
```python
>>> joined = base.join(URL('') / "_msearch")
>>> str(joined)
'https://localhost/_msearch'
>>> joined._val
SplitResult(scheme='https', netloc='localhost', path='/_msearch', query='', fragment='')
```
(note the str of the url patches the stored path)

however the behavior changed in 1.9.10:
```python
>>> joined = base.join(URL('') / "_msearch")
>>> str(joined)
'https://localhost/_msearch'
>>> joined._val
SplitResult(scheme='https', netloc='localhost', path='_msearch', query='', fragment='')
```

note: the strification of the url matches the expected output, however what is stored in the path is now missing the leading slash.  When sent to aiohttp the ends up causing opensearch to not recognize the request and hence breaking existing code.

If this was intended behavior it should not have been done in a patch and a warning issued...however given that the stored path does not match the strification I'd imagine this change in behavior was not intended.

### To Reproduce

1. Tested with python 3.11
2. Run the following code and compare against 1.9.9 and later versions:
```python
import yarl

base = yarl.URL.build(scheme='https', host='localhost', port=443)
(base.join(yarl.URL('') / "_msearch"))._val
```

### Expected behavior

joined url's stored path should contain leading slash

### Logs/tracebacks

```python-traceback
see above
```


### Python Version

```console
Python 3.11.9
```


### multidict Version

```console
Name: multidict
Version: 6.0.5
Summary: multidict implementation
Home-page: https://github.com/aio-libs/multidict
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache 2
Location: /Users/alexmohr/.local/share/virtualenvs/commodities-uyI3VCa_/lib/python3.11/site-packages
Requires: 
Required-by: aiohttp, yarl
```


### yarl Version

```console
Name: yarl
Version: 1.9.10
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: /Users/alexmohr/.local/share/virtualenvs/commodities-uyI3VCa_/lib/python3.11/site-packages
Requires: idna, multidict
Required-by: aiohttp, fbn_async_server
```


### OS

OSX 14.6.1

### Additional context

_No response_

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aio-libs/yarl/pull/1118?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 95.12%. Comparing base [(`c3e4197`)](https://app.codecov.io/gh/aio-libs/yarl/commit/c3e419758ec7693e2c87c1cfcd06065984c36b15?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) to head [(`5a32096`)](https://app.codecov.io/gh/aio-libs/yarl/commit/5a32096b23167ccddc32308aabb5abec5508016d?dropdown=coverage&el=desc&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    #1118   +/-   ##
=======================================
  Coverage   95.12%   95.12%           
=======================================
  Files          30       30           
  Lines        4633     4640    +7     
  Branches      410      411    +1     
=======================================
+ Hits         4407     4414    +7     
  Misses        200      200           
  Partials       26       26           
```

| [Flag](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/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/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `95.08% <100.00%> (+<0.01%)` | :arrow_up: |
| [MyPy](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `41.11% <37.50%> (-0.03%)` | :arrow_down: |
| [OS-Linux](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.38% <100.00%> (+<0.01%)` | :arrow_up: |
| [OS-Windows](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/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: |
| [OS-macOS](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.08% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.10.11](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `98.97% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.10.14](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.21% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.11.9](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.21% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.12.5](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.21% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.13.0-rc.1](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.21% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.8.10](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `98.92% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.8.18](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.16% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.9.13](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `98.92% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.9.19](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.16% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-pypy7.3.11](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.22% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-pypy7.3.16](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.21% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-pypy7.3.17](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/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: |
| [VM-macos-latest](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.08% <100.00%> (+<0.01%)` | :arrow_up: |
| [VM-ubuntu-latest](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.38% <100.00%> (+<0.01%)` | :arrow_up: |
| [VM-windows-latest](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/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: |
| [pytest](https://app.codecov.io/gh/aio-libs/yarl/pull/1118/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.38% <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/1118?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).


Patch:
diff --git a/CHANGES/1118.bugfix.rst b/CHANGES/1118.bugfix.rst
new file mode 100644
index 0000000..adac595
--- /dev/null
+++ b/CHANGES/1118.bugfix.rst
@@ -0,0 +1,3 @@
+Fixed joining a path when the existing path was empty -- by :user:`bdraco`.
+
+A regression in :meth:`URL.join() <yarl.URL.join>` was introduced in :issue:`1082`.
diff --git a/yarl/_url.py b/yarl/_url.py
index 8289c2a..94f3169 100644
--- a/yarl/_url.py
+++ b/yarl/_url.py
@@ -1379,8 +1379,10 @@ class URL:
         if not other_val.path:
             return URL(val._replace(**parts), encoded=True)
 
-        if other_val.path[0] == "/" or not val.path:
+        if other_val.path[0] == "/":
             path = other_val.path
+        elif not val.path:
+            path = f"/{other_val.path}"
         elif val.path[-1] == "/":
             path = f"{val.path}{other_val.path}"
         else:


Test patch:
diff --git a/tests/test_url.py b/tests/test_url.py
index 4089b36..5f6e033 100644
--- a/tests/test_url.py
+++ b/tests/test_url.py
@@ -1767,6 +1767,14 @@ def test_join_cpython_urljoin(base, url, expected):
     assert joined == expected
 
 
+def test_join_preserves_leading_slash():
+    """Test that join preserves leading slash in path."""
+    base = URL.build(scheme="https", host="localhost", port=443)
+    new = base.join(URL("") / "_msearch")
+    assert str(new) == "https://localhost/_msearch"
+    assert new.path == "/_msearch"
+
+
 def test_empty_authority():
     assert URL("http:///").authority == ""