36 min read

Cache Media-Heavy Localhost Previews Safely

Design safe caching for media previews over a Localtonet HTTP tunnel without masking updates, leaking private content, or hiding development bugs.

Development Guides ยท Media Preview Caching ยท Localtonet ยท 2026

Make remote media previews faster without letting stale files, private responses, or hidden application defects mislead reviewers

A media-heavy application can feel instant on localhost but slow down when every image, video, audio file, or document repeatedly travels through a developer's upload connection. Selective HTTP caching reduces unnecessary transfers, but an overly broad policy can conceal updates, retain sensitive responses, and make a preview behave unlike the application under test. This guide explains the cache boundary, provides a complete Python reference server with validators and byte-range support, verifies it locally and through a Localtonet HTTP tunnel, and covers bypass, invalidation, routine operation, and cleanup. Localtonet supplies the public route to the local service, while the application and any separately configured cache remain responsible for response policy.

๐Ÿ”’ Keep private and personalized responses out of shared caches ๐ŸŒ Test standard HTTP behavior through a public tunnel โšก Reuse immutable media without masking active development

Understand the preview architecture before adding a cache

A typical remote preview begins with an application listening on a local IP address and port. The Localtonet client runs on a device that can reach that application, establishes an outbound connection to one of our relay servers, and provides a public URL for an HTTP tunnel. A reviewer requests that URL, and the request reaches the local service through the running tunnel. This avoids inbound router port forwarding, direct firewall changes, VPN setup, and the need for a public IP address.

That public route does not by itself determine whether a browser, service worker, reverse proxy, or CDN may store a response. HTTP caching is controlled primarily by the response metadata emitted by the application and by any deliberately configured cache between the browser and origin. Localtonet should therefore be treated as the transport path in this design, not as an implicit media cache.

Remote browser traffic reaches a localhost media preview through a Localtonet HTTP tunnel and controlled cache boundary.
The tunnel exposes the local preview, while authentication and explicit cache policy determine how each response may be handled.

This separation of responsibilities matters during diagnosis. If a stale image appears, investigate the requested URL, origin headers, browser cache, service worker, and any separately configured shared cache. Do not assume that the tunnel transformed the asset. Likewise, adding a public tunnel does not make an application response safe to cache. Authorization, tenant isolation, cookies, content sensitivity, and update behavior remain application concerns.

๐Ÿ–ฅ๏ธ Local origin The development server generates HTML, API output, and media. It identifies which representations are public, private, mutable, or immutable.
๐ŸŒ Localtonet HTTP tunnel Our client connects outward to a selected relay and routes the assigned public URL to the configured local IP address and port while the tunnel is running.
๐Ÿ“ฆ Optional shared cache A reverse proxy or CDN may reuse eligible responses for multiple viewers, but only when it is configured separately and the response is safe to share.
๐Ÿ’ป Browser or service-worker storage A browser can reuse local responses, revalidate stale entries, or let application service-worker code answer before a request reaches the network.

Caching helps most when the same large, unchanged object is requested repeatedly. A replayed video, a font used on every page, or a stable image opened by several reviewers can be a good candidate. Server-rendered HTML, live API output, authenticated media, and files being edited every few minutes usually need more conservative treatment.

A cache hit changes what the preview measures

Once a browser, service worker, or shared cache can answer a request, that request may no longer exercise the local origin or the complete tunnel path. This is useful for realistic warm-cache review, but it is not a substitute for cold-load testing. Test both states so caching does not hide slow generation, broken delivery, or an inadequate upload connection.

Classify every response by sensitivity and mutability

Response classification map based on content sensitivity and how often the media changes.
Sensitivity and mutability determine whether a response can be cached, revalidated, kept private, or not stored.

Safe caching starts with classification, not a list of file extensions. An extension can suggest a media type, but it does not establish whether the response is public, personalized, or stable. Two JPEG responses can require opposite policies. A public fingerprinted background image may be safely reusable for a long time, while a temporary account document rendered as a JPEG should not enter a shared cache.

For each route family, ask four questions: Does the representation contain confidential or user-specific information? Can two authorized users receive different bytes from the same URL? How quickly can the content change? Does the URL change whenever the bytes change? These answers define the cache policy more reliably than object size alone.

Response class Typical examples Recommended starting policy Main risk
Fingerprint-addressed public asset Versioned image, font, script, poster, or media file whose URL changes with its content Long-lived public caching can be appropriate, potentially with immutable Reusing the URL after changing its bytes defeats the design
Stable public asset at a fixed URL Public logo, sample audio, or reusable preview image Short freshness or no-cache, plus a correct validator A long freshness lifetime can conceal an updated file
Actively edited media Draft image, video, audio mix, generated PDF, or 3D asset no-cache with an ETag, or no-store during sensitive diagnosis Reviewers may comment on an obsolete revision
HTML and application shell Development page, server-rendered route, or entry document Usually no-store for live development previews Stored HTML can reference old bundles or conceal server-side changes
Public API data Non-personal catalog data or public metadata A deliberately short policy only when temporary staleness is acceptable Incorrect freshness can hide state changes and API defects
Authenticated or personalized response Private documents, account media, user thumbnails, or signed-in HTML Default to private, no-store unless a reviewed design proves otherwise Cross-user disclosure through shared storage
Error or diagnostic response Authorization failure, missing asset, stack trace, or temporary server error Do not intentionally cache during development A repair can appear ineffective, or diagnostics can persist

A practical default is to allow aggressive caching only when content-derived or build-derived names make the URL immutable. Keep HTML, API output, authenticated media, errors, and frequently edited assets conservative. Expand the cacheable set route by route after confirming that the request target and any relevant representation dimensions uniquely identify the response.

A query parameter normally contributes to the URI, but a separately configured intermediary can normalize or omit selected parameters from its cache key. If revision identity matters, a filename or path containing a content hash is usually easier to audit than an undocumented query-key rule. Never assume that adding a random query string repairs a flawed cache configuration.

Build a standards-based cache header policy

The Cache-Control response header communicates whether a response may be stored and how it may be reused. Validators such as ETag and Last-Modified identify representations so clients can make conditional requests. The normative caching model is defined by HTTP Caching, RFC 9111, while validators, conditional requests, and range requests are defined in HTTP Semantics, RFC 9110.

Freshness and validation solve different problems. A fresh response can be reused without contacting the origin. A stale response with a validator can be checked, potentially producing 304 Not Modified without retransmitting the body. A policy should state which behavior is intended rather than relying on browser defaults.

Use no-store when storage is undesirable

Cache-Control: no-store

Under RFC 9111's no-store semantics, a compliant cache must not store the response and must make a best effort to remove it from volatile storage as soon as practical. This is appropriate for sensitive previews, authenticated HTML, private documents, debugging output, and routes whose retained content could cause disclosure or confusion.

no-store is not an origin-reachability guarantee or purge command

A newly returned no-store directive governs that response when processed by compliant caches. It does not guarantee that every request reaches the application, erase a response stored under an older policy, clear a service worker's application-managed cache, remove downloaded files, or force a noncompliant intermediary to behave correctly. A previously stored response may answer before the new policy is received. When correcting an unsafe policy, invalidate shared caches using their documented controls, clear or update service-worker storage, use a fresh browser profile for verification, and rotate sensitive URLs where appropriate.

Use no-cache when storage is acceptable but reuse requires validation

Cache-Control: no-cache
ETag: "representation-revision"

Despite its name, no-cache permits storage. It requires successful validation before reuse. A client can send the stored ETag in If-None-Match, and the origin can answer with 304 Not Modified when the selected representation is unchanged. Conditional request evaluation is specified in RFC 9110's conditional request section.

This is useful for mutable, non-sensitive media. Reviewers check for updates on reuse, while unchanged objects avoid another full body transfer. The validator must change whenever the selected representation changes. A constant or incorrectly reused ETag can hide an update just as effectively as an excessive freshness lifetime.

Use long-lived public caching only with immutable URLs

Cache-Control: public, max-age=31536000, immutable

A fingerprinted URL binds an asset location to one revision. When bytes change, the build or export process publishes a different filename and updates references to it. The one-year lifetime above is an example policy, not a universal requirement. The critical condition is that the bytes at that URL do not change. If a development process overwrites a fingerprinted file, the URL is not immutable and the policy becomes unsafe.

Separate browser and shared-cache freshness only when necessary

Cache-Control: public, max-age=60, s-maxage=600

max-age defines freshness broadly, while s-maxage applies to shared caches. A distinct shared lifetime can be useful when a browser should revalidate sooner than an explicitly configured shared cache. Only use it when the response is safe for multiple users. Large size does not make a private representation safe to share.

Choose validators that identify the actual representation

An ETag is an opaque representation identifier. A strong ETag indicates that the representation data is byte-for-byte equivalent for the relevant comparison. A weak ETag, prefixed with W/, indicates semantic equivalence without promising identical bytes. The distinction and validator rules are covered by RFC 9110's validator requirements.

Last-Modified can be convenient for file-backed assets, but timestamp precision and deployment behavior may fail to distinguish rapid changes. A strong ETag derived from the response bytes is more precise, although hashing a large file has a computation cost. The reference implementation below caches hashes in memory until the file size or modification timestamp changes.

Use Vary only for genuine representation dimensions

If request headers select different representations at one URL, a cache needs to include the relevant fields when choosing a stored response. The Vary field definition in RFC 9110 describes this relationship. Vary: Accept-Encoding is common when encoded and unencoded representations differ, while content negotiation may involve Accept.

Avoid broad variation on volatile headers because it can produce many entries with little reuse. More importantly, Vary is not access control. If authorization, tenant identity, or a session changes the body, a publicly shared cache is usually the wrong design.

Protect authenticated, personalized, and sensitive media

The most serious cache failure is serving one user's private representation to another user. This can happen when an intermediary stores an authenticated response under a key that omits authorization context, tenant identity, or another input that changed the body.

For ordinary remote development previews, begin with Cache-Control: private, no-store on authenticated and personalized routes. The private directive prevents storage by shared caches, while no-store instructs compliant caches not to store the response at all.

Cache-Control: private, no-store

Do not add raw session identifiers, access tokens, or signed credentials to public cache keys. That can expose secrets in logs and storage while leaving subtle isolation failures. If an advanced system genuinely needs shared caching for protected media, it requires a separately reviewed design with strict key isolation, controlled lifetimes, authorization-aware invalidation, and defenses against cache poisoning and cache deception.

Security condition Safe preview default Required verification
Response depends on login or session cookie private, no-store; bypass shared caching Signed-out and second-account requests cannot receive the body
Response contains an expiring signed URL Treat as private unless the complete retention model is reviewed Stored content cannot outlive the intended authorization boundary
Public immutable media Long-lived public caching at a content-specific URL Changing bytes always creates a new URL
Mutable public preview no-cache with a changing validator An edit changes the ETag and returns the new body
Error or diagnostic output no-store Repairing the origin is visible immediately in a controlled cold test
Unknown route class Do not admit it to shared storage Policy is reviewed before cacheability is expanded

Signed URLs also require care. An expiring signature does not automatically prevent a stored response from surviving beyond the authorization window. Cookies present a similar problem. If a response sets a session cookie or depends on one, do not make it publicly cacheable without a deliberate and tested architecture.

Keep the Localtonet device authentication token secret. It identifies the client device that runs the tunnel and should never appear in source code, public HTML, screenshots, terminal recordings shared with reviewers, cache keys, or example commands. Apply the same rule to application credentials and private endpoints.

Choose between browser, reverse-proxy, and remote caching

Comparison of browser, reverse-proxy, and CDN cache locations in front of a localhost preview origin.
Cache placement changes who can reuse a response and where validation occurs.

Not every preview needs an additional shared cache. Browser caching is the smallest place to start because it primarily benefits repeated requests by the same reviewer. A local reverse proxy can centralize policy and avoid repeated generation. A remotely operated shared cache can avoid repeated origin transfer after a successful fill, but it creates another storage, security, and invalidation boundary.

Layer Who benefits Good fit Key limitation
Browser cache One browser profile or user agent Repeated review by the same person and conditional validation Another reviewer fetches the object independently
Service worker Clients controlled by the application's worker logic Purpose-built offline or application caching Can answer independently of HTTP cache expectations and retain old entries
Local reverse proxy Requests that reach the local proxy Header normalization, controlled cache experiments, and shielding an expensive generator Responses still traverse the developer's upload connection
Remote shared cache Multiple remote reviewers Public, stable, large assets requested repeatedly Requires separate cache keys, purge controls, security review, and operational ownership
No storage No reuse Sensitive debugging, live HTML, authorization checks, and rapidly changing output Every network request that reaches the path requires a new response

A local reverse proxy can reduce repeated rendering or file reads, but it does not necessarily reduce internet upload usage. If it runs behind the tunnel on the same local network, each response still leaves that network. A remote cache may avoid later origin transfers, but its exact behavior belongs to that separately selected service and configuration. This tutorial does not attribute caching, WebSocket handling, framework integration, or CDN behavior to the Localtonet HTTP tunnel without current product evidence.

Account for byte-range requests

Video, audio, and large documents may use the Range request header to request only part of a selected representation. Under RFC 9110's range-request rules, a server that supports a satisfiable byte range can return 206 Partial Content. A single-range response includes Content-Range, identifying the returned interval and complete representation length.

A server is not required to honor every Range request. It can ignore the field and return the full representation with 200 OK. An unsatisfiable byte range normally produces 416 Range Not Satisfiable with Content-Range: bytes */complete-length. A valid 206 response must describe the partial bytes, not a complete body mislabeled as partial content.

Validators matter when a client resumes or seeks within changing media. If-Range lets a client request a range only if the representation still matches its validator. If it does not match, the server sends the full current representation rather than combining old and new bytes. Strong validators are appropriate for this comparison. Do not assume every browser or shared cache stores and combines partial responses identically. Verify the origin's protocol behavior, then check the documentation and observed behavior of every added cache layer.

Run a complete application-level caching reference server

The following implementation is deliberately narrow. It is a static media-preview server for Python 3.11 or later, using only the standard library. It demonstrates three policy classes, strong SHA-256 ETags, If-None-Match, single byte ranges, If-Range, HEAD, conservative error handling, and an explicit diagnostic bypass. It is suitable for learning and controlled previews, not as a general production web server.

Prerequisites and directory layout

  • Python 3.11 or later available as python3.
  • curl for protocol verification.
  • A Localtonet client only when you reach the public-tunnel stage.
  • Media you are authorized to expose to the intended reviewers.

Create a working directory with mutable media in preview/media and content-versioned assets in preview/assets/immutable:

mkdir -p media-preview/preview/media
mkdir -p media-preview/preview/assets/immutable
cd media-preview

printf '<!doctype html><html><body><h2>Media preview</h2><img src="/media/draft.jpg" alt="Draft"></body></html>' > preview/index.html

# Copy your own test files into these locations:
# preview/media/draft.jpg
# preview/media/sample.mp4
# preview/assets/immutable/logo.CONTENT_HASH.webp

The server intentionally refuses paths beginning with /private/. It does not implement authentication, so private material must not be placed in the published directory. Real application authorization must happen before a protected response is returned.

Save the server as preview_server.py

from __future__ import annotations

import hashlib
import mimetypes
import os
from email.utils import formatdate, parsedate_to_datetime
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, unquote, urlsplit

ROOT = (Path(__file__).parent / "preview").resolve()
HOST = "127.0.0.1"
PORT = 8000
ETAG_CACHE = {}

def strong_etag(path: Path, stat_result: os.stat_result) -> str:
    key = (str(path), stat_result.st_size, stat_result.st_mtime_ns)
    cached = ETAG_CACHE.get(key)
    if cached:
        return cached

    digest = hashlib.sha256()
    with path.open("rb") as source:
        for block in iter(lambda: source.read(1024 * 1024), b""):
            digest.update(block)

    value = '"' + digest.hexdigest() + '"'
    ETAG_CACHE.clear()
    ETAG_CACHE[key] = value
    return value

def parse_single_range(value: str, size: int):
    if not value.startswith("bytes=") or "," in value:
        return None

    specification = value[6:].strip()
    if "-" not in specification:
        return None

    first, last = specification.split("-", 1)

    try:
        if first == "":
            suffix_length = int(last)
            if suffix_length <= 0:
                raise ValueError
            start = max(size - suffix_length, 0)
            end = size - 1
        else:
            start = int(first)
            end = size - 1 if last == "" else int(last)
            if start < 0 or end < start:
                raise ValueError
            if start >= size:
                return "unsatisfiable"
            end = min(end, size - 1)
    except ValueError:
        return None

    return start, end

def if_range_matches(value: str, etag: str, modified_seconds: int) -> bool:
    value = value.strip()

    if value.startswith('"') or value.startswith("W/"):
        return value == etag and not value.startswith("W/")

    try:
        supplied = parsedate_to_datetime(value)
        return int(supplied.timestamp()) >= modified_seconds
    except (TypeError, ValueError, OverflowError):
        return False

class PreviewHandler(BaseHTTPRequestHandler):
    server_version = "MediaPreview/1.0"

    def send_preview_error(self, status: int, message: str):
        body = (message + "\n").encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        if self.command != "HEAD":
            self.wfile.write(body)

    def resolve_file(self):
        parsed = urlsplit(self.path)
        request_path = unquote(parsed.path)

        if request_path.startswith("/private/"):
            return None, parsed

        relative = request_path.lstrip("/")
        candidate = (ROOT / relative).resolve()

        try:
            candidate.relative_to(ROOT)
        except ValueError:
            return None, parsed

        if candidate.is_dir():
            candidate = candidate / "index.html"

        if not candidate.is_file():
            return None, parsed

        return candidate, parsed

    def cache_control(self, request_path: str, query: dict) -> str:
        if query.get("cache_bypass") == ["1"]:
            return "no-store"

        if request_path == "/" or request_path.endswith(".html"):
            return "no-store"

        if request_path.startswith("/assets/immutable/"):
            return "public, max-age=31536000, immutable"

        return "no-cache"

    def do_HEAD(self):
        self.serve_file(send_body=False)

    def do_GET(self):
        self.serve_file(send_body=True)

    def serve_file(self, send_body: bool):
        path, parsed = self.resolve_file()
        if path is None:
            self.send_preview_error(404, "Not found")
            return

        stat_result = path.stat()
        size = stat_result.st_size
        modified_seconds = int(stat_result.st_mtime)
        etag = strong_etag(path, stat_result)
        query = parse_qs(parsed.query)
        policy = self.cache_control(parsed.path, query)
        content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"

        if_none_match = self.headers.get("If-None-Match")
        if if_none_match:
            candidates = [item.strip() for item in if_none_match.split(",")]
            if "*" in candidates or etag in candidates:
                self.send_response(304)
                self.send_header("ETag", etag)
                self.send_header("Last-Modified", formatdate(modified_seconds, usegmt=True))
                self.send_header("Cache-Control", policy)
                self.end_headers()
                return

        selected_range = None
        range_header = self.headers.get("Range")
        if range_header:
            if_range = self.headers.get("If-Range")
            if if_range is None or if_range_matches(if_range, etag, modified_seconds):
                selected_range = parse_single_range(range_header, size)

        if selected_range == "unsatisfiable":
            self.send_response(416)
            self.send_header("Content-Range", f"bytes */{size}")
            self.send_header("Cache-Control", "no-store")
            self.send_header("Content-Length", "0")
            self.end_headers()
            return

        if isinstance(selected_range, tuple):
            start, end = selected_range
            response_length = end - start + 1
            self.send_response(206)
            self.send_header("Content-Range", f"bytes {start}-{end}/{size}")
        else:
            start, end = 0, size - 1
            response_length = size
            self.send_response(200)

        self.send_header("Content-Type", content_type)
        self.send_header("Content-Length", str(response_length))
        self.send_header("Accept-Ranges", "bytes")
        self.send_header("ETag", etag)
        self.send_header("Last-Modified", formatdate(modified_seconds, usegmt=True))
        self.send_header("Cache-Control", policy)
        self.send_header("X-Content-Type-Options", "nosniff")
        self.end_headers()

        if not send_body:
            return

        with path.open("rb") as source:
            source.seek(start)
            remaining = response_length
            while remaining:
                block = source.read(min(64 * 1024, remaining))
                if not block:
                    break
                self.wfile.write(block)
                remaining -= len(block)

if __name__ == "__main__":
    if not ROOT.is_dir():
        raise SystemExit(f"Preview directory does not exist: {ROOT}")

    server = ThreadingHTTPServer((HOST, PORT), PreviewHandler)
    print(f"Serving {ROOT} at http://{HOST}:{PORT}")
    print("Press Ctrl+C to stop.")

    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\nStopping preview server.")
    finally:
        server.server_close()

Start the local preview

python3 preview_server.py

Keep this terminal open. The service listens only on 127.0.0.1:8000. A Localtonet client running on the same device can target that address. If the client runs on another device, the service must listen on an address reachable from that client, and the change should be made only with an understanding of the resulting LAN exposure.

Verify the policy locally before opening a tunnel

curl -sS -D - -o /dev/null http://127.0.0.1:8000/
curl -sS -D - -o /dev/null http://127.0.0.1:8000/media/draft.jpg
curl -sS -D - -o /dev/null http://127.0.0.1:8000/assets/immutable/logo.CONTENT_HASH.webp

The HTML response should contain Cache-Control: no-store. Mutable media should contain Cache-Control: no-cache and an ETag. The fingerprinted asset should contain the long-lived public policy. If a test file does not exist, the server returns a 404 with no-store.

Copy the ETag from the mutable media response and test conditional validation:

curl -sS -D - -o /dev/null \
  -H 'If-None-Match: "ETAG-FROM-FIRST-RESPONSE"' \
  http://127.0.0.1:8000/media/draft.jpg

An unchanged file should produce 304 Not Modified. Replace the file, repeat the request, and confirm that the ETag changes and the response returns the new representation.

Use the diagnostic bypass carefully

curl -sS -D - -o /dev/null \
  'http://127.0.0.1:8000/media/draft.jpg?cache_bypass=1'

When this request reaches the reference server, its response uses no-store. The parameter does not purge an earlier response and cannot force a service worker or misconfigured intermediary to contact the server. For a controlled cold test, combine it with a fresh browser profile or disabled browser cache, inspect service-worker registrations, and verify server logs to establish that the origin was reached.

The reference server is intentionally not an authentication system

Publish only files intended for the review audience. Do not add confidential material to the preview directory, and do not interpret a difficult-to-guess URL as authorization. For protected previews, use an application that authenticates every request before reading or returning private media.

Expose the verified origin with a Localtonet HTTP tunnel

Add remote access only after the local tests pass. The Localtonet client must run on a device that can reach the preview service. Creating a tunnel does not start it, and the public route remains available only while the selected client is connected and the tunnel is running.

1

Install and run the Localtonet client

Install the Localtonet application for the operating system on the device that can reach 127.0.0.1:8000, then run the client. Use the current installation instructions presented by our platform rather than an unverified command from an older article.

2

Open the HTTP tunnel configuration

Go to the HTTP tunnel page in the current Localtonet dashboard. HTTP tunnels point to a local IP address and port reachable from the selected client device.

3

Select the Process Type

Choose Random Sub Domain, Custom Sub Domain, or Custom Domain as appropriate and available for your account. All three provide a public HTTPS address for the configured content. Check current Localtonet documentation before configuring custom-domain DNS because exact requirements can change.

4

Select the AuthToken and relay server

Select the device-specific authentication token for the client that will run the tunnel, then choose an available server or region shown in the dashboard. Treat the token as a secret, and do not assume that every server, region, or option is included in every plan.

5

Enter the local IP address and port

For this reference implementation, use 127.0.0.1 and port 8000 when the Localtonet client runs on the same machine. If you are tunneling another application, use the actual address and port reachable from the client device.

6

Start the tunnel

Use the Start control after creating the tunnel. Confirm that the selected client is connected and the tunnel is running before opening the assigned public HTTPS URL.

For current dashboard details, use our Localtonet HTTP tunnel documentation. The documented HTTP workflow establishes the route to the local target. Application-framework compatibility, development host checks, persistent connections, and custom-domain DNS behavior should be verified against the current documentation for the specific software involved rather than assumed.

A public preview is an internet-facing application

Remove debug consoles, directory listings, secrets, unsafe administrative actions, and test accounts that should not be public. Require application authentication for restricted previews and apply least privilege. A tunnel avoids inbound port forwarding, but it does not make an unauthenticated application private or replace origin authorization.

Verify local, public, conditional, and range behavior

Network inspection panels for cold, cached, validated, and unauthorized media requests.
Network evidence distinguishes full transfers, cache reuse, conditional validation, and blocked private-media access.

Replace PUBLIC-URL in the examples with the HTTPS hostname assigned to the running tunnel. Do not include credentials in shell history or copied commands.

Compare local and public response metadata

curl -sS -D - -o /dev/null \
  http://127.0.0.1:8000/media/draft.jpg

curl -sS -D - -o /dev/null \
  https://PUBLIC-URL/media/draft.jpg

Compare the status, Cache-Control, ETag, Last-Modified, Content-Type, Content-Length, and Accept-Ranges fields. The reference implementation gives HEAD and GET consistent metadata, but a different framework may handle them through separate code paths. When uncertain, use a normal GET and discard the body as shown above.

Verify conditional requests through the public URL

curl -sS -D - -o /dev/null \
  -H 'If-None-Match: "ETAG-FROM-FIRST-RESPONSE"' \
  https://PUBLIC-URL/media/draft.jpg

An unchanged representation should produce 304 Not Modified without a media body. Replace the local file and repeat the request. The ETag should change, and a request using the old validator should receive the current representation rather than 304.

Compare full and partial representations

First request the complete object:

curl -sS -D - -o /dev/null \
  https://PUBLIC-URL/media/sample.mp4

The reference server should return 200 OK, the full Content-Length, an ETag, and no Content-Range. Next request a satisfiable single range:

curl -sS -D - -o /dev/null \
  -H 'Range: bytes=0-1023' \
  https://PUBLIC-URL/media/sample.mp4

For a file of at least 1,024 bytes, the reference server returns 206 Partial Content, Content-Length: 1024, and a field shaped like Content-Range: bytes 0-1023/COMPLETE-LENGTH. The ETag should identify the same selected representation as the full response.

Test an unsatisfiable range:

curl -sS -D - -o /dev/null \
  -H 'Range: bytes=999999999999-' \
  https://PUBLIC-URL/media/sample.mp4

The expected result is 416 Range Not Satisfiable with Content-Range: bytes */COMPLETE-LENGTH. A different conforming origin may ignore a malformed range and send a full 200; do not treat every non-206 response as a tunnel failure.

Verify If-Range with matching and stale validators

curl -sS -D - -o /dev/null \
  -H 'Range: bytes=0-1023' \
  -H 'If-Range: "CURRENT-ETAG"' \
  https://PUBLIC-URL/media/sample.mp4

curl -sS -D - -o /dev/null \
  -H 'Range: bytes=0-1023' \
  -H 'If-Range: "STALE-ETAG"' \
  https://PUBLIC-URL/media/sample.mp4

With the current strong ETag, the reference server returns the requested 206. With a stale ETag, it ignores the range and returns the complete current representation with 200. This prevents a client from combining bytes from two revisions. Browser seeking should also be tested near the beginning, middle, and end because command-line checks do not exercise player buffering behavior.

Run the cache and security correctness matrix

Test Expected observation Bug it detects
First public media load Correct body, MIME type, status, and intended cache fields Routing, metadata, or policy errors
Immediate repeat Reuse or validation consistent with the declared policy Missing cacheability or unexpected storage
Edit mutable media The validator changes and the next checked request receives the new revision Stale validators or excessive freshness
Publish fingerprinted media References use a new URL while the old URL retains its original bytes Overwriting immutable objects
Signed-out request for private media Access is denied without disclosing the body Missing origin authorization
Second authorized account It cannot receive the first account's personalized representation Shared-cache isolation failure
Origin file removed Observed behavior matches the documented expiry or invalidation plan Persisting sensitive or obsolete copies
Controlled cold profile Server logs show the request reaching the origin through the tunnel Performance defects hidden by stored responses
Satisfiable byte range 206 with an accurate Content-Range Broken seeking or incorrect partial metadata
Stale If-Range A complete current 200 representation Combining partial bytes from different revisions

Browser developer tools can distinguish transferred bytes from some locally reused resources, but use a fresh profile or controlled cache setting for cold tests. Service workers require separate inspection because their fetch handlers and storage APIs can answer independently of ordinary HTTP cache processing. A single fast reload is not proof of correct caching.

Handle bypass, invalidation, routine operation, and cleanup

Use two explicit review modes

Maintain a warm-cache mode for normal visual review and a cold or bypassed mode for origin diagnosis. Record which mode produced a result. A warm request measures the user experience after storage or validation. A controlled cold request measures local generation, media reads, upload capacity, tunnel transit, and first-response behavior.

Sending Cache-Control: no-cache in a request asks caches to validate according to HTTP rules, but it should not be treated as a universal purge or proof that application code ran. Browser reload controls also vary. Stronger evidence includes a fresh profile, disabled service worker, server request logs, and a new versioned URL.

Invalidate immutable assets by publishing a new URL

Do not overwrite /assets/immutable/logo.CONTENT_HASH.webp. Generate a new content hash, save the replacement under the new name, and update the HTML reference. The previous URL can remain cached because it still identifies the previous bytes. This avoids dependence on a successful global purge.

Invalidate mutable media through validation or an explicit purge

For /media/draft.jpg, replace the file and verify that the strong ETag changes. Clients using no-cache should validate before reuse and receive the current representation. If a separately configured shared cache ignored or overrode the origin policy, use that provider's documented purge mechanism. There is no universal purge command for all reverse proxies, browsers, service workers, and CDNs.

Changing a route from a cacheable policy to no-store does not retroactively erase existing entries. Purge shared storage, update service-worker cache names or deletion logic, use new URLs for sensitive replacements, and tell reviewers when local browser data must be cleared.

Use a repeatable startup checklist

  • Confirm that only intended files are inside the preview directory.
  • Start the reference server and verify http://127.0.0.1:8000/.
  • Run the local header, validator, and range checks.
  • Start the Localtonet client and then start the configured HTTP tunnel.
  • Test the assigned public URL from a separate browser profile or network.
  • Share the public URL only with the intended review audience.
  • Monitor the local server terminal for unexpected paths or request volume.

Shut down and clean up after review

Stop the Localtonet tunnel when public access is no longer needed. Stop the Python server with Ctrl+C. If the tunnel will not be reused, delete it from the dashboard. Remove temporary preview media, especially anything copied from a protected environment. If sensitive content was accidentally exposed, stopping the route is only the first containment step. Review application logs, invalidate any separately configured shared cache, rotate affected URLs or credentials, and account for copies that may remain in browser or service-worker storage.

To remove the complete reference project after stopping its processes, delete the media-preview working directory using the normal file-management tools for your operating system. Inspect the directory before deletion if it contains the only copy of any media.

Troubleshoot stale content and slow media systematically

The asset stays stale after an edit

Inspect the requested URL first. If it was advertised as immutable but its file was overwritten, publish a new fingerprinted URL. If the URL is intentionally mutable, inspect its freshness policy and validator. Confirm that the ETag changes after the edit. Then examine each possible storage layer: service worker, browser, local proxy, remote cache, and origin.

Do not add random query strings as a permanent repair. They can create unbounded entries, can be ignored by a customized cache key, and do not correct the underlying versioning mistake.

The same asset downloads in full on every request

Check whether the response uses no-store, lacks a validator, or produces a different ETag on identical bytes. Confirm that the client sends If-None-Match and that the server evaluates it. Authorization and cookies may also cause a deliberate shared-cache bypass.

Private media appears for the wrong reviewer

Treat this as a security incident. Stop or bypass the shared cache, stop the public tunnel if necessary, and remove access to the affected route. Review origin authorization, cache keys, cookies, response directives, logs, and stored objects. Use the cache provider's documented invalidation controls, while recognizing that centrally deleting every browser copy may not be possible.

Video starts but seeking fails

Inspect requests for Range and responses for 206, an accurate Content-Range, a partial Content-Length, and a stable validator. Compare local and public requests. If behavior changes only after adding a separate cache, consult that cache's documented treatment of complete and partial representations rather than assuming uniform behavior.

The first load remains slow

A cache cannot serve an object it has not stored. The first request can still be constrained by local generation time, disk reads, media size, and upload capacity. Optimize source dimensions and encoding, avoid serving assets larger than the preview requires, and preload only genuinely critical media. Caching improves eligible repeated access, not the inherent cost of the initial response.

Local access works but the public URL does not

Confirm that the Localtonet client is connected, the tunnel has been started, and its local IP and port match the listening service. The reference server uses 127.0.0.1:8000, so the client must run on the same machine. For another application, verify its bind address and any host or origin checks according to that application's documentation. Do not broadly disable host protection without understanding the security consequences.

The public page works, but framework live updates do not

Live-update channels are separate from cacheable media delivery and may use persistent or framework-specific connections. Do not pass them through a cache rule intended for images or videos. Their exact requirements depend on the framework, development server, proxy, and current Localtonet behavior. This reference implementation intentionally covers ordinary HTTP GET and HEAD requests and does not claim WebSocket or framework-specific compatibility.

A new no-store response appears, but old content is still visible

Determine whether the request reached the origin. An existing service-worker entry, browser entry, shared-cache object, open document, or downloaded file may still supply the old bytes. Clear or update the responsible storage layer, use the shared cache's purge facility where applicable, unregister or revise the service worker, and publish a new URL when immediate separation from the old object is required.

Diagnose one layer at a time

Prove the origin locally, then test the Localtonet URL, then add any separate shared cache. Comparing these stages is faster and safer than changing origin headers, tunnel settings, service-worker code, and cache rules simultaneously.

Frequently asked questions

Does a Localtonet HTTP tunnel automatically cache my images or videos?

No automatic media caching behavior is established by the supplied Localtonet product documentation. Treat our HTTP tunnel as the public route to the local origin. Define response policy in the application or a deliberate reverse proxy, and configure any remote shared cache separately.

What is the safest default for a live development preview?

Use no-store for HTML, authenticated responses, errors, diagnostics, and routes under active debugging. Use validation for mutable public media. Reserve long-lived public caching for assets whose URLs change whenever their bytes change.

Does no-store guarantee that every request reaches my application?

No. It tells compliant caches not to store the response they receive, but it does not purge older entries, control application-managed service-worker storage, remove downloaded copies, or force a noncompliant intermediary to contact the origin. Verify origin reachability using controlled browser state and server logs.

What is the difference between no-cache and no-store?

no-store instructs compliant caches not to store the response. no-cache permits storage but requires successful validation before reuse. For mutable public media, no-cache with a correct ETag can avoid retransmitting unchanged bytes.

Should authenticated media be stored in a shared cache?

Not by default. Shared caching of authenticated media requires a reviewed isolation and invalidation design. For an ordinary development preview, enforce authorization at the origin, return private, no-store, and keep access tokens and session secrets out of cache keys.

Are query-string version parameters enough for invalidation?

They can distinguish request URIs, but a separately configured cache may normalize or ignore selected parameters. Content-hashed filenames or paths are usually clearer for immutable media. Verify the actual cache-key behavior and never overwrite bytes behind a URL declared immutable.

Must every valid Range request produce 206 Partial Content?

No. An origin can ignore a Range field and return a complete 200 representation. When it honors a satisfiable byte range, the 206 response must include accurate partial-response metadata. Unsatisfiable ranges normally produce 416 with the complete length identified in Content-Range.

Will a local reverse-proxy cache reduce internet upload usage?

It can reduce repeated generation or file reads, but a response from a proxy behind the tunnel still crosses the local internet connection. Avoiding repeated upload for multiple reviewers requires a separately configured remote cache that is allowed to store and serve the public object.

Does stopping the Localtonet tunnel remove cached copies?

No. Stopping the tunnel ends the active public route to the origin, but it is not a purge operation for browsers, service workers, downloaded files, or separately configured shared caches. Invalidate content through the controls of the layer that stored it.

Test your cache boundary through a Localtonet HTTP tunnel

Verify the origin locally, expose only the intended application port, and compare complete, conditional, bypassed, and range requests through the assigned public URL. Keep private responses uncacheable, publish immutable replacements at new URLs, and stop the tunnel when the review session ends.

Get Started Free โ†’

Corrections & updates

Substantive changes approved by the Localtonet editorial team are listed transparently below.

Remove the outer article wrapper from Model.Body and move the hero to the beginning, with the existing clickable guide card immediately after it. Relocate the opening architecture figure to the relevant educational section. Add standards citations and qualify no-store so it is not presented as an absolute origin-reachability guarantee; explain that changing to no-store does not purge existing browser, service-worker, or shared-cache entries. Add one clearly scoped and tested implementation path, such as application-level headers or a

Localtonet is a secure multi-protocol tunneling and proxy platform designed to expose localhost, devices, private services, and AI agents to the public internet supporting HTTP/HTTPS tunnels, TCP/UDP forwarding, mobile proxy infrastructure, file server publishing, latency-optimized game connectivity, and developer-ready AI agent endpoint exposure from a single unified control plane.

support