Repository: gohugoio/hugo (language: go)

Issue:
404 redirects with hugo server fail in multilingual mode on Windows
<details>
<summary>structure</summary>

```text
config/
├── _default/
│   └── config.toml
└── development/
    └── server.toml
content/
├── en/
│   └── _index.md
└── es/
    └── _index.md
layouts/
├── _default/
│   ├── baseof.html
│   ├── home.html
│   ├── list.html
│   └── single.html
└── 404.html
```
</details>

<details>
<summary>config\_default\config.toml</summary>

```toml
baseURL = 'https://example.org/'
title = 'Hugo Forum Topic #40568'

defaultContentLanguageInSubdir = true

[languages.en]
contentDir = 'content/en'
languageCode = 'en-US'
languageName = 'English'
weight = 1

[languages.es]
contentDir = 'content/es'
languageCode = 'es-ES'
languageName = 'Espanol'
weight = 2
```
</details>

<details>
<summary>config\development\server.toml</summary>

```toml
[[redirects]]
from = '/en/**'
to = '/en/404.html'
status = 404

[[redirects]]
from = '/es/**'
to = '/es/404.html'
status = 404
```
</details>

---

See:
<https://github.com/gohugoio/hugo/blob/master/commands/server.go#L439>

I suspect that this:

```
file, err := fs.Open(filepath.FromSlash(strings.TrimPrefix(redirect.To, u.Path)))
```

Should be this:

```
file, err := fs.Open(strings.TrimPrefix(redirect.To, u.Path))
```

---

MRE:

```bash
git clone --single-branch -b hugo-forum-topic-40568 https://github.com/jmooring/hugo-testing hugo-forum-topic-40568
cd hugo-forum-topic-40568
hugo server
```

Reference:
<https://discourse.gohugo.io/t/404-pages-on-localhost-for-multilingual-site-failing/40568/15>


Discussion / hints:
>file, err := fs.Open(filepath.FromSlash(strings.TrimPrefix(redirect.To, u.Path)))

I doubt that that is the culprit.
> I doubt that that is the culprit.

The `err` value is "http: invalid character in file path"
>The err value is "http: invalid character in file path"

I will boot up my Windows VM and have a look tomorrow ... It's Windows 8 and every time i boot it I get a million update notifications.



Fix patch:
diff --git a/commands/server.go b/commands/server.go
index 06b7612fcc5..da6313f17b9 100644
--- a/commands/server.go
+++ b/commands/server.go
@@ -436,7 +436,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
 					switch redirect.Status {
 					case 404:
 						w.WriteHeader(404)
-						file, err := fs.Open(filepath.FromSlash(strings.TrimPrefix(redirect.To, u.Path)))
+						file, err := fs.Open(strings.TrimPrefix(redirect.To, u.Path))
 						if err == nil {
 							defer file.Close()
 							io.Copy(w, file)


Test patch:
diff --git a/commands/server_test.go b/commands/server_test.go
index a2b306e54a8..a02eb3c8bbc 100644
--- a/commands/server_test.go
+++ b/commands/server_test.go
@@ -57,32 +57,87 @@ func TestServer404(t *testing.T) {
 
 	r := runServerTest(c,
 		serverTestOptions{
-			test404:     true,
+			pathsToGet:  []string{"this/does/not/exist"},
 			getNumHomes: 1,
 		},
 	)
 
 	c.Assert(r.err, qt.IsNil)
-	c.Assert(r.content404, qt.Contains, "404: 404 Page not found|Not Found.")
+	pr := r.pathsResults["this/does/not/exist"]
+	c.Assert(pr.statusCode, qt.Equals, http.StatusNotFound)
+	c.Assert(pr.body, qt.Contains, "404: 404 Page not found|Not Found.")
 }
 
-// Issue 10287.
-func TestServerUnicode(t *testing.T) {
-	if htesting.IsCI() {
-		// This test is flaky on CI for some reason.
+func TestServerPathEncodingIssues(t *testing.T) {
+	if htesting.IsGitHubAction() {
+		// This test is flaky on CI for some reason. Run it on Windows only for now.
 		// TODO(bep)
-		t.Skip("Skipping test on CI")
+		if runtime.GOOS != "windows" {
+			t.Skip("Skipping test on CI")
+		}
 	}
 	c := qt.New(t)
 
-	r := runServerTest(c,
-		serverTestOptions{
-			pathsToGet: []string{"hügö/"},
-		},
-	)
+	// Issue 10287
+	c.Run("Unicode paths", func(c *qt.C) {
+		r := runServerTest(c,
+			serverTestOptions{
+				pathsToGet: []string{"hügö/"},
+			},
+		)
+
+		c.Assert(r.err, qt.IsNil)
+		c.Assert(r.pathsResults["hügö/"].body, qt.Contains, "This is hügö")
+	})
+
+	// Issue 10314
+	c.Run("Windows multilingual 404", func(c *qt.C) {
+		config := `
+baseURL = 'https://example.org/'
+title = 'Hugo Forum Topic #40568'
+
+defaultContentLanguageInSubdir = true
+
+[languages.en]
+contentDir = 'content/en'
+languageCode = 'en-US'
+languageName = 'English'
+weight = 1
+
+[languages.es]
+contentDir = 'content/es'
+languageCode = 'es-ES'
+languageName = 'Espanol'
+weight = 2
+
+[server]
+[[server.redirects]]
+from = '/en/**'
+to = '/en/404.html'
+status = 404
+
+[[server.redirects]]
+from = '/es/**'
+to = '/es/404.html'
+status = 404
+`
+		r := runServerTest(c,
+			serverTestOptions{
+				config:     config,
+				pathsToGet: []string{"en/this/does/not/exist", "es/this/does/not/exist"},
+			},
+		)
+
+		c.Assert(r.err, qt.IsNil)
+		pr1 := r.pathsResults["en/this/does/not/exist"]
+		pr2 := r.pathsResults["es/this/does/not/exist"]
+		c.Assert(pr1.statusCode, qt.Equals, http.StatusNotFound)
+		c.Assert(pr2.statusCode, qt.Equals, http.StatusNotFound)
+		c.Assert(pr1.body, qt.Contains, "404: 404 Page not found|Not Found.")
+		c.Assert(pr2.body, qt.Contains, "404: 404 Page not found|Not Found.")
+
+	})
 
-	c.Assert(r.err, qt.IsNil)
-	c.Assert(r.pathsResults["hügö/"], qt.Contains, "This is hügö")
 }
 func TestServerFlags(t *testing.T) {
 	c := qt.New(t)
@@ -191,12 +246,14 @@ baseURL="https://example.org"
 			opts := serverTestOptions{
 				config:      test.config,
 				getNumHomes: test.numservers,
-				test404:     true,
+				pathsToGet:  []string{"this/does/not/exist"},
 				args:        args,
 			}
 
 			r := runServerTest(c, opts)
-			c.Assert(r.content404, qt.Contains, "404: 404 Page not found|Not Found.")
+			pr := r.pathsResults["this/does/not/exist"]
+			c.Assert(pr.statusCode, qt.Equals, http.StatusNotFound)
+			c.Assert(pr.body, qt.Contains, "404: 404 Page not found|Not Found.")
 			test.assert(c, r)
 
 		})
@@ -210,12 +267,16 @@ type serverTestResult struct {
 	homesContent   []string
 	content404     string
 	publicDirnames map[string]bool
-	pathsResults   map[string]string
+	pathsResults   map[string]pathResult
+}
+
+type pathResult struct {
+	statusCode int
+	body       string
 }
 
 type serverTestOptions struct {
 	getNumHomes int
-	test404     bool
 	config      string
 	pathsToGet  []string
 	args        []string
@@ -225,7 +286,7 @@ func runServerTest(c *qt.C, opts serverTestOptions) serverTestResult {
 	dir := createSimpleTestSite(c, testSiteConfig{configTOML: opts.config})
 	result := serverTestResult{
 		publicDirnames: make(map[string]bool),
-		pathsResults:   make(map[string]string),
+		pathsResults:   make(map[string]pathResult),
 	}
 
 	sp, err := helpers.FindAvailablePort()
@@ -280,24 +341,18 @@ func runServerTest(c *qt.C, opts serverTestOptions) serverTestResult {
 		func() {
 			resp, err := http.Get(fmt.Sprintf("http://localhost:%d/%s", port, path))
 			c.Assert(err, qt.IsNil)
-			c.Assert(resp.StatusCode, qt.Equals, http.StatusOK)
+			pr := pathResult{
+				statusCode: resp.StatusCode,
+			}
+
 			if err == nil {
 				defer resp.Body.Close()
-				result.pathsResults[path] = helpers.ReaderToString(resp.Body)
+				pr.body = helpers.ReaderToString(resp.Body)
 			}
+			result.pathsResults[path] = pr
 		}()
 	}
 
-	if opts.test404 {
-		resp, err := http.Get(fmt.Sprintf("http://localhost:%d/this-page-does-not-exist", port))
-		c.Assert(err, qt.IsNil)
-		c.Assert(resp.StatusCode, qt.Equals, http.StatusNotFound)
-		if err == nil {
-			defer resp.Body.Close()
-			result.content404 = helpers.ReaderToString(resp.Body)
-		}
-	}
-
 	time.Sleep(1 * time.Second)
 
 	select {