Caddy Server with FastAPI

1. The problem I’m having:

I am serving a FastAPI to send an email. I want to make my API public using Caddy. I have the FastAPI server running with uvicorn and everything works great when I send a POST to localhost:9000. However, I cannot get the API working from my public url and site served through caddy.

2. Error messages and/or full log output:

Successful API call

curl -vL -X POST http://localhost:9000/ \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "name=John Doe" \
    -d "company=Acme Corp" \
    -d "phone=1234567890" \
    -d "email=johndoe@example.com" \
    -d "availability=Monday to Friday"
Note: Unnecessary use of -X or --request, POST is already inferred.
* Host localhost:9000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:9000...
* connect to ::1 port 9000 from ::1 port 34196 failed: Connection refused
*   Trying 127.0.0.1:9000...
* Connected to localhost (127.0.0.1) port 9000
> POST / HTTP/1.1
> Host: localhost:9000
> User-Agent: curl/8.5.0
> Accept: */*
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 104
> 
< HTTP/1.1 200 OK
< date: Fri, 01 Nov 2024 16:04:17 GMT
< server: uvicorn
< content-length: 41
< content-type: application/json
< 
* Connection #0 to host localhost left intact
{"message":"Form submitted successfully"}

Unsuccessful call from the public domain and path

curl -vL -X POST http://cleanmybuilding.co/submit-form \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "name=John Doe" \
    -d "company=Acme Corp" \
    -d "phone=1234567890" \
    -d "email=johndoe@example.com" \
    -d "availability=Monday to Friday"
Note: Unnecessary use of -X or --request, POST is already inferred.
* Host cleanmybuilding.co:80 was resolved.
* IPv6: (none)
* IPv4: 12.235.102.86
*   Trying 12.235.102.86:80...
* Connected to cleanmybuilding.co (12.235.102.86) port 80
> POST /submit-form HTTP/1.1
> Host: cleanmybuilding.co
> User-Agent: curl/8.5.0
> Accept: */*
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 104
> 
< HTTP/1.1 308 Permanent Redirect
< Connection: close
* Please rewind output before next send
< Location: https://cleanmybuilding.co/submit-form
< Server: Caddy
< Date: Fri, 01 Nov 2024 16:09:58 GMT
< Content-Length: 0
< 
* Closing connection
* Clear auth, redirects to port from 80 to 443
* Issue another request to this URL: 'https://cleanmybuilding.co/submit-form'
* Host cleanmybuilding.co:443 was resolved.
* IPv6: (none)
* IPv4: 12.235.102.86
*   Trying 12.235.102.86:443...
* Connected to cleanmybuilding.co (12.235.102.86) port 443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /usr/lib/ssl/cert.pem
*  CApath: /usr/lib/ssl/certs
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=cleanmybuilding.co
*  start date: Nov  1 15:07:41 2024 GMT
*  expire date: Jan 30 15:07:40 2025 GMT
*  subjectAltName: host "cleanmybuilding.co" matched cert's "cleanmybuilding.co"
*  issuer: C=US; O=Let's Encrypt; CN=E6
*  SSL certificate verify ok.
*   Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA384
*   Certificate level 1: Public key type EC/secp384r1 (384/192 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 2: Public key type RSA (4096/152 Bits/secBits), signed using sha256WithRSAEncryption
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://cleanmybuilding.co/submit-form
* [HTTP/2] [1] [:method: POST]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: cleanmybuilding.co]
* [HTTP/2] [1] [:path: /submit-form]
* [HTTP/2] [1] [user-agent: curl/8.5.0]
* [HTTP/2] [1] [accept: */*]
* [HTTP/2] [1] [content-type: application/x-www-form-urlencoded]
* [HTTP/2] [1] [content-length: 104]
> POST /submit-form HTTP/2
> Host: cleanmybuilding.co
> User-Agent: curl/8.5.0
> Accept: */*
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 104
> 
< HTTP/2 404 
< access-control-expose-headers: X-User-ID
< alt-svc: h3=":443"; ma=2592000
< content-type: application/json
< date: Fri, 01 Nov 2024 16:09:57 GMT
< server: Caddy
< server: uvicorn
< content-length: 22
< 
* Connection #1 to host cleanmybuilding.co left intact
{"detail":"Not Found"}

Ive also tried curl with http://cleanmybuilding.co/submit-form/ and the same result happens

3. Caddy version:

caddy version
v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=

4. How I installed and ran Caddy:

Running from Caddyfile with caddy run

a. System environment:

Running Caddy on Ubuntu 24 Headless

b. FastAPI App:

# Brian Lesko
# FastAPI Email API
import sys
sys.path.append('/home/lesko')
from fastapi import FastAPI, Form
from pydantic import BaseModel
from fastapi.middleware.cors import CORSMiddleware
from libemail import SimpleEmailSender
from libstyle import get_email_template, get_button

app = FastAPI()

# Allow only your domain
origins=["*"] # https://cleanmybuilding.co

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["POST"],
    allow_headers=["*"],
)

# Define form data structure
class FormData(BaseModel):
    name: str
    company: str
    phone: str
    email: str
    availability: str

@app.post("/")
async def submit_form(
    name: str = Form(...),
    company: str = Form(...),
    phone: str = Form(...),
    email: str = Form(...),
    availability: str = Form(...)
):
    # Construct email content
    email_content = f"""\
    Name: {name}
    Company: {company}
    Phone: {phone}
    Email: {email}
    Availability: {availability}
    """

    # Initialize and send email using SimpleEmailSender
    subject = "Quote Request"
    content = f"""
        <h1>Customer Quote Request</h1>
        <h2>{name} with {company}is requesting a quote</h2>
        <h3>Email: {email} Phone: {phone}</h3>
        <h3>Availability: {availability}</h3>
        <h3>Please follow up with the customer as soon as possible</h3>
    """
    body = get_email_template(subject,content)
    SimpleEmailSender(subject, body, 'email@mail.com').send_email()
    return {"message": "Form submitted successfully"}

d. My complete Caddy config:

(basic-auth) {
	route {
		header X-User-ID {http.auth.user.id}
	}
	basic_auth {
		email hashed-password
	}
}

cleanmybuilding.co {
	root * /home/lesko/public-website
	file_server
	header Access-Control-Expose-Headers "X-User-ID"

	handle_path /employees/pto/request* {
		import basic-auth
		reverse_proxy 0.0.0.0:8502
	}

	handle_path /employees/pto/total* {
		import basic-auth
		reverse_proxy 0.0.0.0:8503
	}

	handle_path /employees/pto/approve* {
		import basic-auth
		reverse_proxy 0.0.0.0:8504
	}

	handle_path /employees/pto/calendar* {
		import basic-auth
		reverse_proxy 0.0.0.0:8505
	}

	handle_path /employees/pto/manage* {
		import basic-auth
		reverse_proxy 0.0.0.0:8506
	}

	handle_path /employees/supplies* {
		import basic-auth
		reverse_proxy 0.0.0.0:8507
	}

	handle_path /employees/query_sql* {
		import basic-auth
		reverse_proxy 0.0.0.0:8508
	}

	handle_path /employees/database* {
		import basic-auth
		reverse_proxy 0.0.0.0:8509
	}

	handle_path /employees/chat* {
		import basic-auth
		reverse_proxy 0.0.0.0:8510
	}

	handle_path /employees/supplies/approve* {
		import basic-auth
		reverse_proxy 0.0.0.0:8511
	}

	handle_path /employees/reports/manager* {
		import basic-auth
		reverse_proxy 0.0.0.0:8512
	}

	handle_path /employees* {
		import basic-auth
		reverse_proxy 0.0.0.0:8001
	}

	route /submit-form* {
    reverse_proxy 0.0.0.0:9000
	}	
}

5. Links to relevant resources:

Those are requests with different paths. Your upstream is responding with a 404 because it doesn’t know what /submit-form is.

Are you expecting Caddy to strip the matched part before proxying? If so, use handle_path to do so, not route.

I don’t understand why you have so many individual handle_path + reverse_proxy in your config. You don’t just have a single upstream app? Why doesn’t your upstream app do path-based routing?

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.