Skip to content

Commit 3969b00

Browse files
Add no-cache headers to avoid browser caching (#575)
1 parent 3e44094 commit 3969b00

File tree

7 files changed

+64
-12
lines changed

7 files changed

+64
-12
lines changed

aiohttp_devtools/cli.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,24 @@ def cli() -> None:
2626
verbose_help = 'Enable verbose output.'
2727
livereload_help = ('Whether to inject livereload.js into html page footers to autoreload on changes. '
2828
'env variable AIO_LIVERELOAD')
29+
browser_cache_help = ("When disabled (the default), sends no-cache headers to "
30+
"disable browser caching.")
2931

3032

3133
@cli.command()
3234
@click.argument('path', type=_dir_existing, required=True)
3335
@click.option('--livereload/--no-livereload', envvar='AIO_LIVERELOAD', default=True, help=livereload_help)
3436
@click.option('-p', '--port', default=8000, type=int)
3537
@click.option('-v', '--verbose', is_flag=True, help=verbose_help)
36-
def serve(path: str, livereload: bool, port: int, verbose: bool) -> None:
38+
@click.option("--browser-cache/--no-browser-cache", envvar="AIO_BROWSER_CACHE", default=False,
39+
help=browser_cache_help)
40+
def serve(path: str, livereload: bool, port: int, verbose: bool, browser_cache: bool) -> None:
3741
"""
3842
Serve static files from a directory.
3943
"""
4044
setup_logging(verbose)
41-
run_app(**serve_static(static_path=path, livereload=livereload, port=port))
45+
run_app(**serve_static(static_path=path, livereload=livereload, port=port,
46+
browser_cache=browser_cache))
4247

4348

4449
static_help = "Path of static files to serve, if excluded static files aren't served. env variable: AIO_STATIC_STATIC"
@@ -73,6 +78,8 @@ def serve(path: str, livereload: bool, port: int, verbose: bool) -> None:
7378
@click.option('-p', '--port', 'main_port', envvar='AIO_PORT', type=click.INT, help=port_help)
7479
@click.option('--aux-port', envvar='AIO_AUX_PORT', type=click.INT, help=aux_port_help)
7580
@click.option('-v', '--verbose', is_flag=True, help=verbose_help)
81+
@click.option("--browser-cache/--no-browser-cache", envvar="AIO_BROWSER_CACHE", default=None,
82+
help=browser_cache_help)
7683
@click.argument('project_args', nargs=-1)
7784
def runserver(**config: Any) -> None:
7885
"""

aiohttp_devtools/runserver/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ def __init__(self, *,
4343
app_factory_name: Optional[str] = None,
4444
host: str = INFER_HOST,
4545
main_port: int = 8000,
46-
aux_port: Optional[int] = None):
46+
aux_port: Optional[int] = None,
47+
browser_cache: bool = False):
4748
if root_path:
4849
self.root_path = Path(root_path).resolve()
4950
logger.debug('Root path specified: %s', self.root_path)
@@ -75,6 +76,7 @@ def __init__(self, *,
7576
self.host = 'localhost' if self.infer_host else host
7677
self.main_port = main_port
7778
self.aux_port = aux_port or (main_port + 1)
79+
self.browser_cache = browser_cache
7880
logger.debug('config loaded:\n%s', self)
7981

8082
@property

aiohttp_devtools/runserver/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,12 @@ def runserver(**config_kwargs: Any) -> RunServer:
6666
"shutdown_timeout": 0.01, "access_log_class": AuxAccessLogger}
6767

6868

69-
def serve_static(*, static_path: str, livereload: bool = True, port: int = 8000) -> RunServer:
69+
def serve_static(*, static_path: str, livereload: bool = True, port: int = 8000,
70+
browser_cache: bool = False) -> RunServer:
7071
logger.debug('Config: path="%s", livereload=%s, port=%s', static_path, livereload, port)
7172

72-
app = create_auxiliary_app(static_path=static_path, livereload=livereload)
73+
app = create_auxiliary_app(static_path=static_path, livereload=livereload,
74+
browser_cache=browser_cache)
7375

7476
if livereload:
7577
livereload_manager = LiveReloadTask(static_path)

aiohttp_devtools/runserver/serve.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ async def on_prepare(request: web.Request, response: web.StreamResponse) -> None
6868
response.headers[CONTENT_LENGTH] = str(len(response.body))
6969
app.on_response_prepare.append(on_prepare)
7070

71+
if not config.browser_cache:
72+
@web.middleware
73+
async def no_cache_middleware(request: web.Request, handler: Handler) -> web.StreamResponse:
74+
"""Add no-cache header to avoid browser caching in local development."""
75+
response = await handler(request)
76+
response.headers["Cache-Control"] = "no-cache"
77+
return response
78+
79+
app.middlewares.append(no_cache_middleware)
80+
7181
static_path = config.static_url.strip('/')
7282
if config.infer_host and config.static_path is not None:
7383
# we set the app key even in middleware to make the switch to production easier and for backwards compat.
@@ -226,7 +236,8 @@ async def cleanup_aux_app(app: web.Application) -> None:
226236

227237

228238
def create_auxiliary_app(
229-
*, static_path: Optional[str], static_url: str = "/", livereload: bool = True) -> web.Application:
239+
*, static_path: Optional[str], static_url: str = "/", livereload: bool = True,
240+
browser_cache: bool = False) -> web.Application:
230241
app = web.Application()
231242
app[WS] = set()
232243
app.update(
@@ -248,7 +259,8 @@ def create_auxiliary_app(
248259
static_path + '/',
249260
name='static-router',
250261
add_tail_snippet=livereload,
251-
follow_symlinks=True
262+
follow_symlinks=True,
263+
browser_cache=browser_cache
252264
)
253265
app.router.register_resource(route)
254266

@@ -315,8 +327,10 @@ async def websocket_handler(request: web.Request) -> web.WebSocketResponse:
315327

316328

317329
class CustomStaticResource(StaticResource):
318-
def __init__(self, *args: Any, add_tail_snippet: bool = False, **kwargs: Any):
330+
def __init__(self, *args: Any, add_tail_snippet: bool = False,
331+
browser_cache: bool = False, **kwargs: Any):
319332
self._add_tail_snippet = add_tail_snippet
333+
self._browser_cache = browser_cache
320334
super().__init__(*args, **kwargs)
321335
self._show_index = True
322336

@@ -377,7 +391,8 @@ async def _handle(self, request: web.Request) -> web.StreamResponse:
377391
# Inject CORS headers to allow webfonts to load correctly
378392
response.headers['Access-Control-Allow-Origin'] = '*'
379393

380-
# Add no-cache header to avoid browser caching in local development.
381-
response.headers["Cache-Control"] = "no-cache"
394+
if not self._browser_cache:
395+
# Add no-cache header to avoid browser caching in local development.
396+
response.headers["Cache-Control"] = "no-cache"
382397

383398
return response

tests/test_runserver_main.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,24 @@ async def test_run_app_aiohttp_client(tmpworkdir, aiohttp_client):
123123
cli = await aiohttp_client(app)
124124
r = await cli.get('/')
125125
assert r.status == 200
126+
assert r.headers["Cache-Control"] == "no-cache"
126127
text = await r.text()
127128
assert text == 'hello world'
128129

129130

131+
@forked
132+
async def test_run_app_browser_cache(tmpworkdir, aiohttp_client):
133+
mktree(tmpworkdir, SIMPLE_APP)
134+
config = Config(app_path="app.py", browser_cache=True)
135+
app_factory = config.import_app_factory()
136+
app = await config.load_app(app_factory)
137+
modify_main_app(app, config)
138+
cli = await aiohttp_client(app)
139+
r = await cli.get("/")
140+
assert r.status == 200
141+
assert "Cache-Control" not in r.headers
142+
143+
130144
async def test_aux_app(tmpworkdir, aiohttp_client):
131145
mktree(tmpworkdir, {
132146
'test.txt': 'test value',

tests/test_runserver_serve.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,8 @@ def add_subapp(self, path, app):
136136

137137
def test_modify_main_app_all_off(tmpworkdir):
138138
mktree(tmpworkdir, SIMPLE_APP)
139-
config = Config(app_path='app.py', livereload=False, host='foobar.com', static_path='.')
139+
config = Config(app_path="app.py", livereload=False, host="foobar.com",
140+
static_path=".", browser_cache=True)
140141
app = DummyApplication()
141142
subapp = DummyApplication()
142143
app.add_subapp("/sub/", subapp)
@@ -156,7 +157,7 @@ def test_modify_main_app_all_on(tmpworkdir):
156157
app.add_subapp("/sub/", subapp)
157158
modify_main_app(app, config) # type: ignore[arg-type]
158159
assert len(app.on_response_prepare) == 1
159-
assert len(app.middlewares) == 1
160+
assert len(app.middlewares) == 2
160161
assert app['static_root_url'] == 'http://localhost:8001/static'
161162
assert subapp['static_root_url'] == "http://localhost:8001/static"
162163
assert app._debug is True

tests/test_serve.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ async def test_simple_serve(cli, tmpworkdir):
2121
assert r.status == 200
2222
assert r.headers['content-type'] == 'application/octet-stream'
2323
assert 'Access-Control-Allow-Origin' in r.headers and r.headers['Access-Control-Allow-Origin'] == '*'
24+
assert r.headers["Cache-Control"] == "no-cache"
2425
text = await r.text()
2526
assert text == 'hello world'
2627

@@ -32,6 +33,16 @@ async def test_file_missing(cli):
3233
assert '404: Not Found\n' in text
3334

3435

36+
async def test_browser_cache(event_loop, aiohttp_client, tmpworkdir):
37+
args = serve_static(static_path=str(tmpworkdir), browser_cache=True)
38+
assert args["port"] == 8000
39+
cli = await aiohttp_client(args["app"])
40+
mktree(tmpworkdir, {"foo": "hello world"})
41+
r = await cli.get("/foo")
42+
assert r.status == 200
43+
assert "Cache-Control" not in r.headers
44+
45+
3546
async def test_html_file_livereload(event_loop, aiohttp_client, tmpworkdir):
3647
args = serve_static(static_path=str(tmpworkdir), livereload=True)
3748
assert args["port"] == 8000

0 commit comments

Comments
 (0)