How to Control Your API Response Body in Frappe (`frappe.local.response` vs `frappe.response` )

in short (summary)

use frappe.local.response instead of return

@frappe.whitelist()
def my_api():
    # Your custom logic here
    data = {"status": "success", "data": [1, 2, 3]}
    
    # Option 1: Use frappe.local.response for full control
    frappe.local.response["http_status_code"] = 200
    frappe.local.response["custom_data"] = data
    frappe.local.response.pop("message", None)  # Remove default message
    
    # Option 2: Return data (gets wrapped in "message")
    return data

(deep explaination how it’s done behind the scene) :backhand_index_pointing_down::backhand_index_pointing_down::backhand_index_pointing_down:

Frappe Response Handling Documentation

Overview

Frappe provides two main response handling mechanisms: frappe.local.response and frappe.response. Understanding the difference between these is crucial for building proper API endpoints and controlling response structures in Frappe applications.

Core Concepts

What is Werkzeug?

Werkzeug is a collection of libraries that can be used to create a WSGI (Web Server Gateway Interface) compatible web application in Python.

Werkzeug is the gateway between HTTP and your Python code. When a browser sends an HTTP request, Werkzeug receives it, understands it, and passes it to Frappe in a way Python can work with. Frappe then processes the request (for example, fetching data or rendering a page) and sends the result back through Werkzeug, which turns it into a proper HTTP response for the browser. In short, Werkzeug is what allows your Frappe app to “speak” HTTP.

Thread-Local Storage

Frappe uses Werkzeug’s Local class for thread-local storage, which means each request gets its own isolated storage space. This prevents data leakage between concurrent requests.

from werkzeug.local import Local
local = Local()

Response Initialization

When Frappe initializes for a site, it sets up the response object:

local.response = _dict({"docs": []})

The _dict is Frappe’s custom dictionary class that provides additional functionality.

frappe.local.response

frappe.local.response is the actual response object that gets serialized and sent back to the client. It’s a thread-local dictionary that stores all response data.

Access Pattern

response = local("response")  # Creates a LocalProxy

This creates a LocalProxy that automatically accesses the thread-local response object.

Structure

The default structure of frappe.local.response is:

{
    "docs": [],  # Default empty list for documents
    # Other response data gets added here
}

Key Properties

1. HTTP Status Code

frappe.local.response["http_status_code"] = 200  # Success
frappe.local.response["http_status_code"] = 400  # Bad Request
frappe.local.response["http_status_code"] = 401  # Unauthorized
frappe.local.response["http_status_code"] = 404  # Not Found
frappe.local.response["http_status_code"] = 500  # Internal Server Error

2. Response Type

frappe.local.response["type"] = "json"      # JSON response
frappe.local.response["type"] = "csv"       # CSV download
frappe.local.response["type"] = "pdf"       # PDF download
frappe.local.response["type"] = "binary"    # Binary file download
frappe.local.response["type"] = "redirect"  # Redirect response
frappe.local.response["type"] = "page"      # Web page response

3. File Downloads

frappe.local.response["filename"] = "report.pdf"
frappe.local.response["filecontent"] = pdf_content
frappe.local.response["type"] = "download"

4. Custom Data

# Add any custom data to the response
frappe.local.response["custom_field"] = "custom_value"
frappe.local.response["data"] = {"key": "value"}

frappe.response

frappe.response is a LocalProxy that provides convenient access to frappe.local.response. It’s essentially a shorthand for accessing the thread-local response object.

Access Pattern

# These are equivalent:
frappe.response["message"] = "Hello"
frappe.local.response["message"] = "Hello"

Default Behavior

By default, Frappe’s handler automatically wraps return values in a message field:

# If your function returns "Hello World"
def my_api():
    return "Hello World"

# The response becomes:
{
    "message": "Hello World"
}

Key Differences

Aspect frappe.local.response frappe.response
Type Actual dictionary object LocalProxy (accessor)
Direct Access Yes Yes (via proxy)
Default Wrapping No Yes (adds “message” field)
Control Level Full control Full control
Use Case Custom response structures Standard Frappe responses

Response Flow

1. Request Processing

# Request comes in
# Frappe initializes thread-local storage
local.response = _dict({"docs": []})

2. API Execution

@frappe.whitelist()
def my_api():
    # Your custom logic here
    data = {"status": "success", "data": [1, 2, 3]}
    
    # Option 1: Use frappe.local.response for full control
    frappe.local.response["http_status_code"] = 200
    frappe.local.response["custom_data"] = data
    frappe.local.response.pop("message", None)  # Remove default message
    
    # Option 2: Return data (gets wrapped in "message")
    return data

3. Response Building

# In frappe/utils/response.py
def as_json():
    response = Response()
    if frappe.local.response.http_status_code:
        response.status_code = frappe.local.response["http_status_code"]
        del frappe.local.response["http_status_code"]
    
    response.mimetype = "application/json"
    response.data = json.dumps(frappe.local.response, default=json_handler)
    return response

4. Response Types

Frappe supports multiple response types:

JSON Response

frappe.local.response["type"] = "json"  # Default
# Returns JSON with all data in frappe.local.response

File Download

frappe.local.response["type"] = "download"
frappe.local.response["filename"] = "file.pdf"
frappe.local.response["filecontent"] = pdf_content

Redirect

frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = "/new-url"

Best Practices

1. Use frappe.local.response for Custom APIs

When building custom APIs that need specific response structures:

@frappe.whitelist()
def custom_api():
    try:
        # Your logic here
        result = {"status": "success", "data": [...]}
        
        # Set custom response
        frappe.local.response["http_status_code"] = 200
        frappe.local.response["result"] = result
        frappe.local.response.pop("message", None)  # Remove default
        
    except Exception as e:
        frappe.local.response["http_status_code"] = 500
        frappe.local.response["error"] = str(e)
        frappe.local.response.pop("message", None)

2. Handle Status Codes Properly

Always set appropriate HTTP status codes:

# Success
frappe.local.response["http_status_code"] = 200

# Client Errors
frappe.local.response["http_status_code"] = 400  # Bad Request
frappe.local.response["http_status_code"] = 401  # Unauthorized
frappe.local.response["http_status_code"] = 403  # Forbidden
frappe.local.response["http_status_code"] = 404  # Not Found

# Server Errors
frappe.local.response["http_status_code"] = 500  # Internal Server Error

3. Remove Default Message When Needed

If you want a custom response structure, remove the default message:

frappe.local.response.pop("message", None)

4. Use Consistent Error Handling

def api_with_error_handling():
    try:
        # Your logic
        pass
    except ValidationError as e:
        frappe.local.response["http_status_code"] = 400
        frappe.local.response["error"] = str(e)
        frappe.local.response.pop("message", None)
    except Exception as e:
        frappe.local.response["http_status_code"] = 500
        frappe.local.response["error"] = "Internal server error"
        frappe.local.response.pop("message", None)

Common Use Cases

1. Custom JSON API Response

@frappe.whitelist()
def get_custom_data():
    data = {
        "items": [{"id": 1, "name": "Item 1"}],
        "total": 1,
        "page": 1
    }
    
    frappe.local.response["http_status_code"] = 200
    frappe.local.response["data"] = data
    frappe.local.response.pop("message", None)

2. File Download

@frappe.whitelist()
def download_report():
    pdf_content = generate_pdf()
    
    frappe.local.response["type"] = "download"
    frappe.local.response["filename"] = "report.pdf"
    frappe.local.response["filecontent"] = pdf_content

3. Error Response

@frappe.whitelist()
def api_with_validation():
    if not frappe.form_dict.get("required_field"):
        frappe.local.response["http_status_code"] = 400
        frappe.local.response["error"] = "Required field is missing"
        frappe.local.response.pop("message", None)
        return

4. Redirect Response

@frappe.whitelist()
def redirect_after_action():
    # Process something
    frappe.local.response["type"] = "redirect"
    frappe.local.response["location"] = "/app/success-page"

Examples

Example 1: Device Price API

@frappe.whitelist(allow_guest=True)
def get_device_price(phone: dict) -> dict:
    try:
        # Validation and processing logic...
        
        # Custom response structure
        frappe.local.response["http_status_code"] = 200
        frappe.local.response[device_data] = device_data
        frappe.local.response.pop("message", None)  # Remove default message
        
    except Exception as e:
        frappe.local.response["http_status_code"] = 500
        frappe.local.response["error"] = "An unexpected error occurred"
        frappe.local.response.pop("message", None)

Example 2: Standard Frappe Response

@frappe.whitelist()
def standard_api():
    # This will be wrapped in {"message": "Hello World"}
    return "Hello World"

Example 3: Document List Response

@frappe.whitelist()
def get_documents():
    docs = frappe.get_all("User", fields=["name", "full_name"])
    
    # This will be wrapped in {"message": docs}
    return docs

Response Type Mapping

Frappe maps response types to specific handlers:

response_type_map = {
    "csv": as_csv,
    "txt": as_txt,
    "download": as_raw,
    "json": as_json,      # Default
    "pdf": as_pdf,
    "page": as_page,
    "redirect": redirect,
    "binary": as_binary,
}

Important Notes

  1. Thread Safety: Both frappe.local.response and frappe.response are thread-safe due to Werkzeug’s Local implementation.

  2. Default Behavior: Frappe automatically wraps return values in a message field unless you explicitly control the response structure.

  3. Status Code Handling: HTTP status codes are automatically removed from the response body and set as the actual HTTP status code.

  4. JSON Serialization: Frappe automatically handles JSON serialization of complex objects like dates, decimals, and Frappe documents.

  5. Error Handling: Always handle exceptions and set appropriate status codes for proper API behavior.


LocalProxy Explained

What is LocalProxy?

LocalProxy is a lazy accessor from Werkzeug that provides thread-safe access to thread-local data.

  • Thread-Local Storage
    A mechanism that provides isolated data storage for each thread, ensuring that data from one request does not interfere with another.

  • Proxy Pattern
    A design pattern where a “proxy” object acts as an intermediary to control access to another object — in Frappe, it enables dynamic access to context-specific data.

  • Thread Safety
    The property of code or data that ensures safe execution in a multi-threaded environment without race conditions or data corruption.

  • Request Context
    The environment or scope of a single HTTP request, including data like the current user, session, and database connection.

What is the LocalProxy Pattern?

The LocalProxy pattern is a way to give you an object that looks and feels like a normal variable, but behind the scenes it actually fetches the real value only when you use it. This is especially useful in web frameworks, where each HTTP request needs its own isolated context (request, response, user, etc.).

In Werkzeug, LocalProxy is often used with a Local object (thread-local or greenlet-local storage). This makes sure that when multiple users are making requests at the same time, each one sees their own request/response data instead of accidentally sharing it.

How Frappe Uses LocalProxy with Werkzeug

Frappe uses frappe.local as a request-local object, built on Werkzeug’s Local/LocalProxy system.

  • Every incoming HTTP request gets its own frappe.local.
  • Inside it, Frappe stores things like frappe.local.request, frappe.local.response, frappe.local.session, etc.
  • Because of the LocalProxy pattern, you can safely access frappe.local.response in your code, and you’ll always get the right response object for the current request — even if hundreds of requests are being served at the same time.

In short: The LocalProxy pattern in Werkzeug lets Frappe manage request-specific data (frappe.local) safely across multiple users and requests, without you needing to manually pass around request/response objects.

Simple Definition

# Instead of direct access:
actual_data = local.response

# LocalProxy creates a "promise" to access later:
proxy = local("response")  # Promise to access local.response

How It Works

1. Creation (No Data Access)

response = local("response")  # Creates proxy, doesn't access data yet

2. Usage (Automatic Access)

response["message"] = "Hello"  # Automatically accesses local.response["message"]

Visual Example

from werkzeug.local import Local, LocalProxy

# Create storage
local = Local()

# Create proxy
my_proxy = local("my_data")

# Initialize data
local.my_data = {"name": "John"}

# Use proxy (automatically accesses real data)
print(my_proxy["name"])  # "John"
my_proxy["age"] = 30     # local.my_data["age"] = 30

Frappe Implementation

# In frappe/__init__.py
local = Local()                    # Thread-local storage
response = local("response")       # Create proxy

# Later in init():
local.response = _dict({"docs": []})  # Initialize actual data

# Usage:
frappe.response["message"] = "Hello"  # Proxy accesses local.response

What Happens Behind the Scenes

# When you do this:
frappe.response["message"] = "Hello"

# LocalProxy automatically does this:
frappe.local.response["message"] = "Hello"

Summary

LocalProxy = Smart pointer that:

  • Remembers what data to access
  • Accesses it automatically when used
  • Provides thread safety
  • Makes APIs cleaner

In Frappe: frappe.response is a LocalProxy to frappe.local.response

What local(“response”) Does:

The local(“response”) call creates a LocalProxy object that:

  1. Automatically accesses local.response when you use frappe.response

  2. Is thread-safe - each thread gets its own local.response

  3. Provides transparent access - you can use frappe.response exactly like a dictionary

You can verify this by looking at the type:

import frappe
print(type(frappe.response))  # <class 'werkzeug.local.LocalProxy'>
print(type(frappe.local.response))  # <class 'frappe.types.frappedict._dict'>

So frappe.response is indeed a LocalProxy that automatically accesses frappe.local.response, which is the actual _dict object containing your response data.

This is why both of these work identically:

frappe.response["message"] = "Hello"
frappe.local.response["message"] = "Hello"

The proxy pattern allows Frappe to provide convenient access to thread-local data while maintaining thread safety.


In Short

Use frappe.local.response when you need full control over the response structure, and use frappe.response (or return values) for standard Frappe responses. Always set appropriate HTTP status codes and handle errors properly for a professional API experience.

frappe.local.response vs frappe.response

  • frappe.local.response
    • This is the real response object.
    • It lives inside frappe.local, which is request-specific storage (thanks to Werkzeug’s LocalProxy).
    • Each request gets its own separate frappe.local.response, so multiple users don’t interfere with each other.
  • frappe.response
    • This is just a shortcut (alias) that points to frappe.local.response using the LocalProxy pattern.
    • It’s there to make your code cleaner so you don’t always have to type frappe.local.response.
6 Likes

Thanks for sharing your helpful information as usual

1 Like