Serving UTF-8 static files? Headers to the rescue (an epic tutorial)!


Imagine there’s a PythonAnywhere user, homer8bc, with poetic inclinations. He wants to serve his newest poem (he believes it’s quite epic) as a static text page. He’s old school — he doesn’t believe in HTML, and as for CSS? Forget it! His friend, S. Yodos, lives in Cyme, while homer8bc resides on Ios island, so in-person communication is difficult…

After uploading the first lines of his masterpiece to his account, homer8bc follows the usual steps. He goes to the Web page, adds a new web app using the manual config option, and sets the static files mapping from /story.txt to /home/homer8bc/mysite/story.txt. Then, he reloads the web app.

Feeling quite pleased with himself, homer8bc sends the link to https://homer8bc.pythonanywhere.com/story.txt to his friend. To his surprise, S. Yodos replies, baffled. He explains that he was up late last night writing tutorials for peasants, and his tired eyes must be playing tricks on him because the “great poem” looks more like worms wriggling in his garden:

That’s certainly not what our poet had in mind! But, determined as ever, homer8bc doesn’t give up. After searching through our documentation, he discovers the solution: using PythonAnywhere’s API to add static headers to the requests for his poem to specify that the text should be rendered as UTF-8!

He first goes to the Account page and clicks the “API Token” tab to create an API token. Then, for the sake of clarity, he adds another static file mapping — from /story-with-headers.txt to the same story.txt file:

Next, homer8bc starts a fresh Python console (so that the API_TOKEN environment variable is available) and runs the following code (Note: you’ll need to adjust some variables to match your setup, if you want to follow in his footsteps!):

import os
import requests

username = "homer8bc"                      # use your username
api_token = os.getenv("API_TOKEN")
host = "www.pythonanywhere.com"            # on EU use "eu.pythonanywhere.com"
domain = f"{username}.pythonanywhere.com"  # on EU use f"{username}.eu.pythonanywhere.com"
static_url = "/story-with-headers.txt"     # use static url set before
authorization_headers = {"Authorization": f"Token {api_token}"}
api_endpoint = f"https://{host}/api/v0/user/{username}/webapps/{domain}/static_headers/"

response = requests.post(
    url=api_endpoint,
    headers=authorization_headers,
    data={"url": static_url, "name": "Content-Type", "value": "text/plain; charset=utf-8"}
)

This code grabs api_token from the environment and sends a POST request to the /static_headers/ API endpoint for homer8bc’s domain. The key part is in the data of the request: the url specifies the static URL defined in the web app’s static file mapping, which in this case is /story-with-headers.txt. Keep in mind, this is a one-to-one mapping. If the mapping were, e.g., from /static to some directory, then all resources in that directory would get the added header.

The name field defines the static header name — here, it’s Content-Type — and the value is set to text/plain; charset=utf-8, which does the trick.

If everything goes well, the response should return a status code of 201, meaning the static header has been successfully added. As a sanity check, it’s possible to a perform a GET request to the static_headers API endpoint to confirm:

response = requests.get(api_endpoint, headers=authorization_headers)
print(response.json())

One should see something like this:

[{'id': 999,
  'url': '/story-with-headers.txt',
  'name': 'Content-Type',
  'value': 'text/plain; charset=utf-8'}]

Seeing that the response matches what he intended, homer8bc reloads the web app using the button on the Web page. Finally, when he visits https://homer8bc.pythonanywhere.com/story-with-headers.txt, the first verses of his poem proudly appear as intended:

Before sending the link to S. Yodos, homer8bc decides to add another 15,689 lines of gore and violence. “Wrath wasn’t such a bad topic after all,” he murmurs to himself. “Worms, you mumble? I’ll give you WAR, then! And ten years of it!” — and the rest is history

comments powered by Disqus