Skip to main content

Overview

The request context keeps track of request-level data during a request. It provides access to request and session objects, and is automatically created and destroyed for each request.

What is Request Context?

Starting with Flask 3.2, request context has been merged into AppContext. The context now includes both application and request data when a request is being handled.
From ctx.py:260-298, the unified context provides:
  • Access to request and session proxies
  • Request-specific data and state
  • Automatic lifecycle management
class AppContext:
    """An app context contains information about an app, and about the request
    when handling a request."""
    
    def __init__(self, app, *, request=None, session=None):
        self.app = app
        self.g = app.app_ctx_globals_class()
        self._request = request
        self._session = session
        # ...
    
    @property
    def has_request(self):
        """True if this context was created with request data."""
        return self._request is not None

When is it Active?

Request context is automatically active during:
from flask import Flask, request

app = Flask(__name__)

@app.route('/')
def index():
    # Request context is active
    print(request.method)  # Works!
    print(request.url)     # Works!
    return 'Hello'

# Outside request - won't work
try:
    print(request.method)  # RuntimeError!
except RuntimeError as e:
    print(e)  # "Working outside of request context"

The request Proxy

From wrappers.py:18-220, access request data:
from flask import request

@app.route('/info')
def request_info():
    # HTTP method
    method = request.method  # GET, POST, etc.
    
    # URL information
    url = request.url                # Full URL
    base_url = request.base_url      # Without query string
    path = request.path              # /info
    query_string = request.query_string  # bytes
    
    # Headers
    user_agent = request.headers.get('User-Agent')
    content_type = request.content_type
    
    # Client info
    remote_addr = request.remote_addr
    referrer = request.referrer
    
    # Routing info
    endpoint = request.endpoint      # 'request_info'
    view_args = request.view_args    # URL parameters dict
    url_rule = request.url_rule      # Matched Rule object
    
    # Blueprint info
    blueprint = request.blueprint    # Current blueprint or None
    
    return jsonify({
        'method': method,
        'path': path,
        'endpoint': endpoint
    })

The session Proxy

From ctx.py:381-403, access session data:
from flask import session

@app.route('/login', methods=['POST'])
def login():
    # Session context is automatically available
    session['user_id'] = 123
    session['logged_in'] = True
    return redirect('/dashboard')

@app.route('/dashboard')
def dashboard():
    # Access session data
    if 'user_id' not in session:
        return redirect('/login')
    
    user_id = session.get('user_id')
    return f'User ID: {user_id}'

Creating Request Context

For Testing

def test_request():
    with app.test_request_context('/'):
        # Request context is active
        assert request.path == '/'
        assert request.method == 'GET'

def test_post_request():
    with app.test_request_context('/', method='POST', data={'key': 'value'}):
        assert request.method == 'POST'
        assert request.form['key'] == 'value'

With Custom Environ

from werkzeug.test import EnvironBuilder

with app.request_context(EnvironBuilder('/', method='POST').get_environ()):
    # Custom request context
    assert request.method == 'POST'

Manual Push/Pop

ctx = app.test_request_context('/')
ctx.push()
try:
    # Do work with request context
    print(request.path)
finally:
    ctx.pop()

Request Context Properties

From ctx.py:350-379:
@property
def request(self):
    """The request object associated with this context."""
    if self._request is None:
        raise RuntimeError("There is no request in this context.")
    return self._request

@property
def session(self):
    """The session object associated with this context."""
    session = self._get_session()
    session.accessed = True  # Mark as accessed
    return session

@property
def has_request(self):
    """True if this context was created with request data."""
    return self._request is not None

Context Locals

Flask uses context-local variables that are isolated per request:
from flask import request, g
import threading

@app.route('/worker/<int:task_id>')
def start_worker(task_id):
    # Each thread gets its own request context
    def worker():
        # This would fail - different thread
        # print(request.path)  # RuntimeError!
        pass
    
    thread = threading.Thread(target=worker)
    thread.start()
    
    return f'Started task {task_id}'

Copying Request Context

From ctx.py:154-206, copy context for background tasks:
from flask import copy_current_request_context
import gevent

@app.route('/background')
def background_task():
    @copy_current_request_context
    def do_work():
        # Has access to request and session
        print(request.method)
        print(session.get('user_id'))
    
    gevent.spawn(do_work)
    return 'Task started'
When copying request context for background tasks:
  • Read request.form, request.json, or request.data before spawning
  • Access session in parent to ensure Vary: Cookie header is set
  • Avoid modifying session in background task

Checking Context State

From ctx.py:209-232:
from flask import has_request_context, request

def get_user_ip():
    if has_request_context():
        return request.remote_addr
    return None

def log_request_info():
    if has_request_context():
        print(f'{request.method} {request.path}')
    else:
        print('No request context')

@app.route('/')
def index():
    ip = get_user_ip()  # Works - returns IP
    return f'Your IP: {ip}'

# Outside request
ip = get_user_ip()  # Returns None

Request Lifecycle

From ctx.py:416-517, the request processing flow:
# 1. Context is pushed
ctx.push()
    # - Creates request object
    # - Opens session
    # - Matches URL to route
    # - Sends appcontext_pushed signal

# 2. Request is processed
    # - before_request functions run
    # - View function executes
    # - after_request functions run

# 3. Context is popped
ctx.pop()
    # - teardown_request functions run
    # - Request object is closed
    # - teardown_appcontext functions run
    # - Sends appcontext_popped signal

URL Matching

From ctx.py:405-414, URL is matched when context is pushed:
def match_request(self):
    """Apply routing to the current request."""
    try:
        result = self.url_adapter.match(return_rule=True)
    except HTTPException as e:
        self._request.routing_exception = e
    else:
        self._request.url_rule, self._request.view_args = result
Access matched route information:
@app.route('/user/<int:user_id>')
def user_profile(user_id):
    print(request.url_rule)        # /user/<int:user_id>
    print(request.endpoint)        # 'user_profile'
    print(request.view_args)       # {'user_id': 123}
    
    return f'User: {user_id}'

After This Request

From ctx.py:118-148, register one-time response processors:
from flask import after_this_request

@app.route('/download')
def download():
    @after_this_request
    def add_download_header(response):
        response.headers['Content-Disposition'] = 'attachment; filename=data.csv'
        return response
    
    return generate_csv()

@app.route('/api/data')
def api_data():
    @after_this_request
    def add_timing_header(response):
        elapsed = time.time() - g.start_time
        response.headers['X-Processing-Time'] = f'{elapsed:.3f}s'
        return response
    
    return jsonify(data)

Request Hooks

Before Request

Run before each request:
@app.before_request
def load_user():
    g.user = None
    if 'user_id' in session:
        g.user = User.query.get(session['user_id'])

@app.before_request
def check_maintenance():
    if app.config.get('MAINTENANCE_MODE'):
        return render_template('maintenance.html'), 503

After Request

Modify response after each request:
@app.after_request
def add_security_headers(response):
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'SAMEORIGIN'
    return response

@app.after_request
def add_cache_header(response):
    if request.endpoint == 'static':
        response.cache_control.max_age = 3600
    return response

Teardown Request

Cleanup after each request:
@app.teardown_request
def close_db_connection(exception):
    db = g.pop('db', None)
    if db is not None:
        if exception is not None:
            db.rollback()
        db.close()

@app.teardown_request
def log_exception(exception):
    if exception is not None:
        app.logger.error(f'Request failed: {exception}')

Common Patterns

Database Connection Per Request

from flask import g

def get_db():
    if 'db' not in g:
        g.db = sqlite3.connect(
            current_app.config['DATABASE'],
            detect_types=sqlite3.PARSE_DECLTYPES
        )
        g.db.row_factory = sqlite3.Row
    return g.db

@app.teardown_request
def close_db(error):
    db = g.pop('db', None)
    if db is not None:
        db.close()

@app.route('/users')
def list_users():
    db = get_db()
    users = db.execute('SELECT * FROM users').fetchall()
    return jsonify([dict(user) for user in users])

Request Timing

import time

@app.before_request
def start_timer():
    g.start_time = time.time()

@app.after_request
def log_request_time(response):
    if hasattr(g, 'start_time'):
        elapsed = time.time() - g.start_time
        if elapsed > 1.0:  # Log slow requests
            current_app.logger.warning(
                f'Slow request: {request.method} {request.path} - {elapsed:.2f}s'
            )
    return response

User Authentication

@app.before_request
def load_logged_in_user():
    user_id = session.get('user_id')
    if user_id is None:
        g.user = None
    else:
        g.user = db.execute(
            'SELECT * FROM user WHERE id = ?', (user_id,)
        ).fetchone()

def login_required(view):
    @functools.wraps(view)
    def wrapped_view(**kwargs):
        if g.user is None:
            return redirect(url_for('login'))
        return view(**kwargs)
    return wrapped_view

@app.route('/profile')
@login_required
def profile():
    return render_template('profile.html', user=g.user)

Context Preservation

For debugging, Flask preserves context on exceptions:
app.config['PRESERVE_CONTEXT_ON_EXCEPTION'] = True

@app.route('/debug')
def debug():
    raise Exception('Debug this!')
    # Request context remains active in debugger

Best Practices

Use Context Checks

Always check has_request_context() in reusable code

Clean Up Resources

Use teardown_request to close connections and files

Be Careful with Threads

Use copy_current_request_context for background tasks

Test with Context

Always use test_request_context in unit tests

Request Context vs Application Context

In Flask 3.2+, both contexts are unified:
Available In ContextObjects
Application Contextcurrent_app, g
Request Contextcurrent_app, g, request, session
# Application context only
with app.app_context():
    print(current_app.name)  # Works
    print(g.value)           # Works
    # print(request.path)    # RuntimeError!

# Request context (includes app context)
with app.test_request_context('/'):
    print(current_app.name)  # Works
    print(g.value)           # Works
    print(request.path)      # Works
    print(session['key'])    # Works

Build docs developers (and LLMs) love