Repository: caddyserver/caddy (language: go)

Issue:
Reverse proxy sporadically 502 when empty curly braces are written on the same line in Caddyfile
## Environment

Windows 10 AMD64

Latest release from `https://caddyserver.com/download`.

  ```bash
./caddy_windows_amd64.exe version
v2.4.6 h1:HGkGICFGvyrodcqOOclHKfvJC0qTU7vny/7FhYp9hNw=
```

I was reverse-proxying for a Vue project's webpack dev-server. All `*.example.com` are aliased to `localhost`.

## Replication

The following works great

```caddyfile
http://local0.example.com {
	reverse_proxy /* localhost:11451 {
		
	}
}
```

But when the curly braces are written on the same line:

```caddyfile
http://local0.example.com {
	reverse_proxy /* localhost:11451 {}
}
```

![](https://i.imgur.com/bBu5xn2.png)

Nearly half of the times client receives HTTP 502, while other requests return normally even with content.

For 502 ones, Caddy logs:

```
2022/01/03 09:00:21.737 ←[31mERROR←[0m  http.log.error  making dial info: upstream {}: invalid dial address : missing port in address   {"request": {"remote_addr": "127.0.0.1:57018", "proto": "HTTP/1.1", "method": "GET", "host": "local0.example.com", "uri": "/js/9.js", "headers": {"Accept": ["application/signed-exchange;v=b3;q=0.7,*/*;q=0.8"], "Accept-Encoding": ["gzip, deflate"], "Accept-Language": ["en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,zh-TW;q=0.6"], "Cookie": ["jguid=9244a986-f872-22a9-5f42-a6c13ebcf75e"], "Connection": ["keep-alive"], "User-Agent": ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36"], "Purpose": ["prefetch"], "Referer": ["http://local0.example.com/"], "If-None-Match": ["W/\"14323-gbbfEHkeFD7peaJ6qEGdT6QDbls\""]}}, "duration": 0, "status": 502, "err_id": "2famxsnvm", "err_trace": "reverseproxy.statusError (reverseproxy.go:886)"}
```



Discussion / hints:
Writing the arguments of the `reverse_proxy` directive in this manner

> 
> ```
> http://local0.example.com {
> 	reverse_proxy /* localhost:11451 {}
> }
> ```
> 

forces the `reverse_proxy` directive to assume the second argument is another upstream. See the second example in the [documentation page](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#examples). There's no need for the curly braces if you're not utilizing any of the sub-directives. Braces on different lines define the beginning and end of a block. Braces within the same line define the use of a [placeholder](https://caddyserver.com/docs/caddyfile/concepts#placeholders). Any Caddy module may define their own placeholder, so it's hard to assume whether what's between the braces is a valid placeholder or plain upstream.

I'm going to close this, but further questions may be asked on the [community forum](https://caddy.community). Thanks for understanding!
After discussing it with the team, we decided to reject empty blocks when parsing.

Fix patch:
diff --git a/caddyconfig/caddyfile/dispenser.go b/caddyconfig/caddyfile/dispenser.go
index fbe71ad02ff..8a78f4337d9 100755
--- a/caddyconfig/caddyfile/dispenser.go
+++ b/caddyconfig/caddyfile/dispenser.go
@@ -458,6 +458,60 @@ func (d *Dispenser) isNewLine() bool {
 	if d.cursor > len(d.tokens)-1 {
 		return false
 	}
-	return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
-		d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
+
+	prev := d.tokens[d.cursor-1]
+	curr := d.tokens[d.cursor]
+
+	// If the previous token is from a different file,
+	// we can assume it's from a different line
+	if prev.File != curr.File {
+		return true
+	}
+
+	// The previous token may contain line breaks if
+	// it was quoted and spanned multiple lines. e.g:
+	//
+	// dir "foo
+	//   bar
+	//   baz"
+	prevLineBreaks := d.numLineBreaks(d.cursor - 1)
+
+	// If the previous token (incl line breaks) ends
+	// on a line earlier than the current token,
+	// then the current token is on a new line
+	return prev.Line+prevLineBreaks < curr.Line
+}
+
+// isNextOnNewLine determines whether the current token is on a different
+// line (higher line number) than the next token. It handles imported
+// tokens correctly. If there isn't a next token, it returns true.
+func (d *Dispenser) isNextOnNewLine() bool {
+	if d.cursor < 0 {
+		return false
+	}
+	if d.cursor >= len(d.tokens)-1 {
+		return true
+	}
+
+	curr := d.tokens[d.cursor]
+	next := d.tokens[d.cursor+1]
+
+	// If the next token is from a different file,
+	// we can assume it's from a different line
+	if curr.File != next.File {
+		return true
+	}
+
+	// The current token may contain line breaks if
+	// it was quoted and spanned multiple lines. e.g:
+	//
+	// dir "foo
+	//   bar
+	//   baz"
+	currLineBreaks := d.numLineBreaks(d.cursor)
+
+	// If the current token (incl line breaks) ends
+	// on a line earlier than the next token,
+	// then the next token is on a new line
+	return curr.Line+currLineBreaks < next.Line
 }
diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go
index 1e694f9f52e..b4632384815 100755
--- a/caddyconfig/caddyfile/parse.go
+++ b/caddyconfig/caddyfile/parse.go
@@ -494,6 +494,13 @@ func (p *parser) directive() error {
 	for p.Next() {
 		if p.Val() == "{" {
 			p.nesting++
+			if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
+				return p.Err("Unexpected next token after '{' on same line")
+			}
+		} else if p.Val() == "{}" {
+			if p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
+				return p.Err("Unexpected '{}' at end of line")
+			}
 		} else if p.isNewLine() && p.nesting == 0 {
 			p.cursor-- // read too far
 			break


Test patch:
diff --git a/caddyconfig/caddyfile/parse_test.go b/caddyconfig/caddyfile/parse_test.go
index 8d43e1aa536..c3f6fa644b9 100755
--- a/caddyconfig/caddyfile/parse_test.go
+++ b/caddyconfig/caddyfile/parse_test.go
@@ -191,6 +191,20 @@ func TestParseOneAndImport(t *testing.T) {
 
 		{``, false, []string{}, []int{}},
 
+		// Unexpected next token after '{' on same line
+		{`localhost
+		  dir1 { a b }`, true, []string{"localhost"}, []int{}},
+		// Workaround with quotes
+		{`localhost
+		  dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}},
+
+		// Unexpected '{}' at end of line
+		{`localhost
+		  dir1 {}`, true, []string{"localhost"}, []int{}},
+		// Workaround with quotes
+		{`localhost
+		  dir1 "{}"`, false, []string{"localhost"}, []int{2}},
+
 		// import with args
 		{`import testdata/import_args0.txt a`, false, []string{"a"}, []int{}},
 		{`import testdata/import_args1.txt a b`, false, []string{"a", "b"}, []int{}},