or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

authentication.mdcore-application.mddata-structures.mdexceptions-status.mdindex.mdmiddleware.mdrequests-responses.mdrouting.mdstatic-files.mdtesting.mdwebsockets.md

static-files.mddocs/

0

# Static File Serving

1

2

Starlette provides efficient static file serving with automatic content type detection, HTTP caching, range request support, and security features for serving CSS, JavaScript, images, and other static assets.

3

4

## StaticFiles Class

5

6

```python { .api }

7

from starlette.staticfiles import StaticFiles

8

from starlette.responses import Response

9

from starlette.types import Scope

10

from typing import List, Optional

11

from os import PathLike

12

import os

13

14

class StaticFiles:

15

"""

16

ASGI application for serving static files.

17

18

Features:

19

- Automatic content type detection

20

- HTTP caching headers (ETag, Last-Modified)

21

- Range request support for partial content

22

- Directory traversal protection

23

- Index file serving

24

- Package-based file serving

25

"""

26

27

def __init__(

28

self,

29

directory: str | PathLike[str] | None = None,

30

packages: List[str | tuple[str, str]] | None = None,

31

html: bool = False,

32

check_dir: bool = True,

33

follow_symlink: bool = False,

34

) -> None:

35

"""

36

Initialize static file server.

37

38

Args:

39

directory: Directory path to serve files from

40

packages: Python packages to serve files from

41

html: Whether to serve .html files for extensionless URLs

42

check_dir: Whether to check directory exists at startup

43

follow_symlink: Whether to follow symbolic links

44

"""

45

self.directory = directory

46

self.packages = packages or []

47

self.html = html

48

self.check_dir = check_dir

49

self.follow_symlink = follow_symlink

50

51

if check_dir:

52

self.check_config()

53

54

def check_config(self) -> None:

55

"""

56

Validate static files configuration.

57

58

Raises:

59

RuntimeError: If configuration is invalid

60

"""

61

62

def get_directories(

63

self,

64

directory: str | None = None,

65

packages: List[str | tuple[str, str]] | None = None,

66

) -> List[PathLike[str]]:

67

"""

68

Get list of directories to serve files from.

69

70

Args:

71

directory: Override directory

72

packages: Override packages

73

74

Returns:

75

List of directory paths to search for files

76

"""

77

78

def get_path(self, scope: Scope) -> str:

79

"""

80

Get file path from request scope.

81

82

Args:

83

scope: ASGI request scope

84

85

Returns:

86

File path relative to served directories

87

"""

88

89

def lookup_path(self, path: str) -> tuple[str | None, os.stat_result | None]:

90

"""

91

Find file in configured directories.

92

93

Args:

94

path: Relative file path

95

96

Returns:

97

Tuple of (full_path, stat_result) or (None, None) if not found

98

"""

99

100

def get_response(self, path: str, scope: Scope) -> Response:

101

"""

102

Create response for file path.

103

104

Args:

105

path: File path to serve

106

scope: ASGI request scope

107

108

Returns:

109

HTTP response for the file

110

"""

111

112

@staticmethod

113

def file_response(

114

full_path: str | PathLike[str],

115

stat_result: os.stat_result,

116

scope: Scope,

117

status_code: int = 200,

118

) -> Response:

119

"""

120

Create optimized file response.

121

122

Args:

123

full_path: Full path to file

124

stat_result: File statistics

125

scope: ASGI request scope

126

status_code: HTTP status code

127

128

Returns:

129

FileResponse or NotModifiedResponse

130

"""

131

132

@staticmethod

133

def is_not_modified(

134

response_headers: Headers,

135

request_headers: Headers,

136

) -> bool:

137

"""

138

Check if file has been modified based on HTTP headers.

139

140

Args:

141

response_headers: Response headers (ETag, Last-Modified)

142

request_headers: Request headers (If-None-Match, If-Modified-Since)

143

144

Returns:

145

True if file has not been modified

146

"""

147

148

class NotModifiedResponse(Response):

149

"""

150

HTTP 304 Not Modified response.

151

152

Returned when client's cached version is still current

153

based on ETag or Last-Modified headers.

154

"""

155

156

def __init__(self, headers: Mapping[str, str] | None = None) -> None:

157

super().__init__(status_code=304, headers=headers)

158

```

159

160

## Basic Static File Serving

161

162

### Simple Directory Serving

163

164

```python { .api }

165

from starlette.applications import Starlette

166

from starlette.routing import Mount

167

from starlette.staticfiles import StaticFiles

168

169

# Serve files from directory

170

app = Starlette(routes=[

171

Mount("/static", StaticFiles(directory="static"), name="static"),

172

])

173

174

# Directory structure:

175

# static/

176

# ├── css/

177

# │ └── style.css

178

# ├── js/

179

# │ └── app.js

180

# └── images/

181

# └── logo.png

182

183

# URLs will be:

184

# /static/css/style.css -> static/css/style.css

185

# /static/js/app.js -> static/js/app.js

186

# /static/images/logo.png -> static/images/logo.png

187

```

188

189

### Multiple Static Directories

190

191

```python { .api }

192

from starlette.routing import Mount

193

194

app = Starlette(routes=[

195

# Different mount points for different asset types

196

Mount("/css", StaticFiles(directory="assets/css"), name="css"),

197

Mount("/js", StaticFiles(directory="assets/js"), name="js"),

198

Mount("/images", StaticFiles(directory="assets/images"), name="images"),

199

200

# General static files

201

Mount("/static", StaticFiles(directory="static"), name="static"),

202

])

203

```

204

205

### Package-based Static Files

206

207

```python { .api }

208

# Serve files from Python package

209

app = Starlette(routes=[

210

Mount("/assets", StaticFiles(packages=["mypackage"]), name="assets"),

211

])

212

213

# Serve from multiple packages

214

app = Starlette(routes=[

215

Mount("/vendor", StaticFiles(packages=[

216

"bootstrap",

217

"jquery",

218

("mylib", "static"), # Serve mylib/static/ directory

219

]), name="vendor"),

220

])

221

222

# Package structure:

223

# mypackage/

224

# ├── __init__.py

225

# ├── static/

226

# │ ├── style.css

227

# │ └── script.js

228

229

# URLs will be:

230

# /assets/style.css -> mypackage/static/style.css

231

# /assets/script.js -> mypackage/static/script.js

232

```

233

234

## Advanced Configuration

235

236

### HTML File Serving

237

238

```python { .api }

239

# Enable HTML mode for SPA (Single Page Application) support

240

app = Starlette(routes=[

241

Mount("/", StaticFiles(directory="dist", html=True), name="spa"),

242

])

243

244

# With html=True:

245

# /about -> looks for dist/about.html or dist/about/index.html

246

# / -> looks for dist/index.html

247

# /dashboard/users -> looks for dist/dashboard/users.html

248

249

# Without html=True (default):

250

# /about.html -> dist/about.html (exact match only)

251

```

252

253

### Symbolic Link Handling

254

255

```python { .api }

256

# Follow symbolic links (security consideration)

257

app = Starlette(routes=[

258

Mount("/static", StaticFiles(

259

directory="static",

260

follow_symlink=True # Default: False for security

261

), name="static"),

262

])

263

264

# Security note: Only enable follow_symlink if you trust

265

# all symbolic links in the directory tree

266

```

267

268

### Directory Validation

269

270

```python { .api }

271

# Disable directory check at startup (for dynamic directories)

272

app = Starlette(routes=[

273

Mount("/uploads", StaticFiles(

274

directory="user_uploads",

275

check_dir=False # Don't validate directory exists at startup

276

), name="uploads"),

277

])

278

279

# Useful when directory might be created after application starts

280

```

281

282

## URL Generation

283

284

### Generating Static File URLs

285

286

```python { .api }

287

from starlette.routing import Route, Mount

288

from starlette.responses import HTMLResponse

289

290

async def homepage(request):

291

# Generate URLs for static files

292

css_url = request.url_for("static", path="css/style.css")

293

js_url = request.url_for("static", path="js/app.js")

294

image_url = request.url_for("static", path="images/logo.png")

295

296

html = f"""

297

<!DOCTYPE html>

298

<html>

299

<head>

300

<link rel="stylesheet" href="{css_url}">

301

</head>

302

<body>

303

<img src="{image_url}" alt="Logo">

304

<script src="{js_url}"></script>

305

</body>

306

</html>

307

"""

308

return HTMLResponse(html)

309

310

app = Starlette(routes=[

311

Route("/", homepage),

312

Mount("/static", StaticFiles(directory="static"), name="static"),

313

])

314

```

315

316

### Template Integration

317

318

```python { .api }

319

from starlette.templating import Jinja2Templates

320

321

templates = Jinja2Templates(directory="templates")

322

323

async def template_page(request):

324

return templates.TemplateResponse("page.html", {

325

"request": request,

326

"title": "My Page"

327

})

328

329

# In templates/page.html:

330

# <link rel="stylesheet" href="{{ url_for('static', path='css/style.css') }}">

331

# <script src="{{ url_for('static', path='js/app.js') }}"></script>

332

```

333

334

## Caching and Performance

335

336

### HTTP Caching Headers

337

338

Static files automatically include caching headers:

339

340

```python { .api }

341

# Automatic headers for static files:

342

# - ETag: Based on file modification time and size

343

# - Last-Modified: File modification timestamp

344

# - Content-Length: File size

345

# - Content-Type: Detected from file extension

346

347

# Client requests with caching headers:

348

# If-None-Match: "etag-value"

349

# If-Modified-Since: "timestamp"

350

351

# Server responds with:

352

# 304 Not Modified (if unchanged)

353

# 200 OK with file content (if changed)

354

```

355

356

### Cache Control

357

358

```python { .api }

359

from starlette.middleware.base import BaseHTTPMiddleware

360

from starlette.responses import Response

361

362

class CacheControlMiddleware(BaseHTTPMiddleware):

363

"""Add Cache-Control headers to static files."""

364

365

def __init__(self, app, max_age: int = 31536000): # 1 year

366

super().__init__(app)

367

self.max_age = max_age

368

369

async def dispatch(self, request, call_next):

370

response = await call_next(request)

371

372

# Add cache headers for static files

373

if request.url.path.startswith("/static/"):

374

response.headers["Cache-Control"] = f"public, max-age={self.max_age}"

375

376

return response

377

378

# Apply to application

379

app.add_middleware(CacheControlMiddleware, max_age=86400) # 1 day

380

```

381

382

### Content Compression

383

384

```python { .api }

385

from starlette.middleware.gzip import GZipMiddleware

386

387

# Add compression for static files

388

app.add_middleware(GZipMiddleware, minimum_size=500)

389

390

# Compresses CSS, JS, HTML, and other text files

391

# Binary files (images, videos) are not compressed

392

```

393

394

## Security Considerations

395

396

### Path Traversal Protection

397

398

```python { .api }

399

# StaticFiles automatically prevents directory traversal

400

# These requests are automatically rejected:

401

# /static/../../../etc/passwd

402

# /static/..%2F..%2Fetc%2Fpasswd

403

# /static/....//....//etc/passwd

404

405

# Safe paths only:

406

# /static/css/style.css ✓

407

# /static/subdir/file.js ✓

408

```

409

410

### Content Type Validation

411

412

```python { .api }

413

import mimetypes

414

from starlette.staticfiles import StaticFiles

415

from starlette.responses import Response

416

417

class SecureStaticFiles(StaticFiles):

418

"""StaticFiles with content type restrictions."""

419

420

ALLOWED_TYPES = {

421

'text/css',

422

'text/javascript',

423

'application/javascript',

424

'image/png',

425

'image/jpeg',

426

'image/gif',

427

'image/svg+xml',

428

'font/woff',

429

'font/woff2',

430

'application/font-woff',

431

'application/font-woff2',

432

}

433

434

def get_response(self, path: str, scope) -> Response:

435

# Get the normal response

436

response = super().get_response(path, scope)

437

438

# Check content type

439

content_type = response.media_type

440

if content_type not in self.ALLOWED_TYPES:

441

# Return 403 for disallowed file types

442

return Response("Forbidden file type", status_code=403)

443

444

return response

445

446

# Use secure static files

447

app = Starlette(routes=[

448

Mount("/static", SecureStaticFiles(directory="static"), name="static"),

449

])

450

```

451

452

### File Size Limits

453

454

```python { .api }

455

class LimitedStaticFiles(StaticFiles):

456

"""StaticFiles with size limits."""

457

458

def __init__(self, *args, max_size: int = 10 * 1024 * 1024, **kwargs):

459

super().__init__(*args, **kwargs)

460

self.max_size = max_size # 10MB default

461

462

def get_response(self, path: str, scope) -> Response:

463

full_path, stat_result = self.lookup_path(path)

464

465

if stat_result and stat_result.st_size > self.max_size:

466

return Response("File too large", status_code=413)

467

468

return super().get_response(path, scope)

469

```

470

471

## Custom Static File Handlers

472

473

### Custom File Processing

474

475

```python { .api }

476

import os

477

from starlette.staticfiles import StaticFiles

478

from starlette.responses import Response

479

480

class ProcessedStaticFiles(StaticFiles):

481

"""StaticFiles with custom processing."""

482

483

def get_response(self, path: str, scope) -> Response:

484

# Custom processing for CSS files

485

if path.endswith('.css'):

486

return self.process_css_file(path, scope)

487

488

# Custom processing for JS files

489

if path.endswith('.js'):

490

return self.process_js_file(path, scope)

491

492

# Default handling for other files

493

return super().get_response(path, scope)

494

495

def process_css_file(self, path: str, scope) -> Response:

496

"""Process CSS files (minification, variable substitution, etc.)"""

497

full_path, stat_result = self.lookup_path(path)

498

499

if not full_path:

500

return Response("Not found", status_code=404)

501

502

# Read and process CSS

503

with open(full_path, 'r') as f:

504

content = f.read()

505

506

# Custom processing (minification, variable replacement, etc.)

507

processed_content = self.minify_css(content)

508

509

return Response(

510

processed_content,

511

media_type="text/css",

512

headers={

513

"Last-Modified": format_date_time(stat_result.st_mtime),

514

"ETag": f'"{stat_result.st_mtime}-{stat_result.st_size}"',

515

}

516

)

517

518

def minify_css(self, content: str) -> str:

519

"""Basic CSS minification."""

520

# Remove comments

521

import re

522

content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)

523

# Remove extra whitespace

524

content = re.sub(r'\s+', ' ', content)

525

return content.strip()

526

```

527

528

### Dynamic File Generation

529

530

```python { .api }

531

import json

532

from datetime import datetime

533

from starlette.responses import JSONResponse

534

535

class DynamicStaticFiles(StaticFiles):

536

"""Serve some files dynamically."""

537

538

def get_response(self, path: str, scope) -> Response:

539

# Generate manifest.json dynamically

540

if path == 'manifest.json':

541

return self.generate_manifest(scope)

542

543

# Generate sitemap.xml dynamically

544

if path == 'sitemap.xml':

545

return self.generate_sitemap(scope)

546

547

return super().get_response(path, scope)

548

549

def generate_manifest(self, scope) -> Response:

550

"""Generate web app manifest."""

551

manifest = {

552

"name": "My Web App",

553

"short_name": "MyApp",

554

"start_url": "/",

555

"display": "standalone",

556

"background_color": "#ffffff",

557

"theme_color": "#000000",

558

"generated_at": datetime.now().isoformat(),

559

}

560

561

return JSONResponse(manifest, headers={

562

"Cache-Control": "no-cache" # Don't cache dynamic content

563

})

564

565

def generate_sitemap(self, scope) -> Response:

566

"""Generate XML sitemap."""

567

base_url = f"{scope['scheme']}://{scope['server'][0]}"

568

569

sitemap = f"""<?xml version="1.0" encoding="UTF-8"?>

570

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">

571

<url>

572

<loc>{base_url}/</loc>

573

<lastmod>{datetime.now().date()}</lastmod>

574

</url>

575

</urlset>"""

576

577

return Response(

578

sitemap,

579

media_type="application/xml",

580

headers={"Cache-Control": "no-cache"}

581

)

582

```

583

584

## Testing Static Files

585

586

### Testing Static File Serving

587

588

```python { .api }

589

from starlette.testclient import TestClient

590

import os

591

592

def test_static_files():

593

# Create test file

594

os.makedirs("test_static", exist_ok=True)

595

with open("test_static/test.css", "w") as f:

596

f.write("body { color: red; }")

597

598

app = Starlette(routes=[

599

Mount("/static", StaticFiles(directory="test_static"), name="static"),

600

])

601

602

client = TestClient(app)

603

604

# Test file serving

605

response = client.get("/static/test.css")

606

assert response.status_code == 200

607

assert response.text == "body { color: red; }"

608

assert response.headers["content-type"] == "text/css"

609

610

# Test 404 for missing file

611

response = client.get("/static/missing.css")

612

assert response.status_code == 404

613

614

# Test caching headers

615

response = client.get("/static/test.css")

616

assert "etag" in response.headers

617

assert "last-modified" in response.headers

618

619

# Test conditional request

620

etag = response.headers["etag"]

621

response = client.get("/static/test.css", headers={

622

"If-None-Match": etag

623

})

624

assert response.status_code == 304

625

626

def test_url_generation():

627

app = Starlette(routes=[

628

Route("/", lambda r: JSONResponse({"css_url": str(r.url_for("static", path="style.css"))})),

629

Mount("/static", StaticFiles(directory="static"), name="static"),

630

])

631

632

client = TestClient(app)

633

response = client.get("/")

634

assert response.json()["css_url"] == "http://testserver/static/style.css"

635

```

636

637

### Performance Testing

638

639

```python { .api }

640

import time

641

import statistics

642

643

def test_static_file_performance():

644

app = Starlette(routes=[

645

Mount("/static", StaticFiles(directory="static"), name="static"),

646

])

647

648

client = TestClient(app)

649

650

# Measure response times

651

times = []

652

for _ in range(100):

653

start = time.time()

654

response = client.get("/static/large_file.js")

655

end = time.time()

656

657

assert response.status_code == 200

658

times.append(end - start)

659

660

# Check performance metrics

661

avg_time = statistics.mean(times)

662

max_time = max(times)

663

664

assert avg_time < 0.1 # Average under 100ms

665

assert max_time < 0.5 # Max under 500ms

666

```

667

668

Static file serving in Starlette provides efficient, secure, and flexible asset delivery with automatic optimization, caching, and security features suitable for production applications.