RSS ke 66 rangkaian sosial: Facebook, Instagram, X, LinkedIn, Telegram dan banyak lagi Blog Afiliasi Hubungi Kami
Log masuk Mula percuma
Updated: 2026-10-22
Tornado RSS Feed: Handler, Built-In ETag and Auto-Post

Short answer: To serve an RSS feed from a Tornado application, add a request handler for /feed.xml, load the newest published rows without blocking the event loop, build the document with ElementTree so every value is escaped, format dates in UTC with the email.utils module, and write the text with the application/rss+xml content type. Tornado computes an ETag from the response body on its own and answers a matching If-None-Match header with a 304, so the only thing you must do is keep the body stable while the content is unchanged. Test the route with curl and a validator, then give the public address to an auto-poster.

Tornado is one of the oldest asynchronous web frameworks for Python. It is still widely used for long-lived connections, internal services and legacy platforms that need to handle many clients on a single process, and it needs no extra packages to serve XML. Services built on it often publish something that people want to follow: articles, release notes, offers, events or listings. A feed is the standard way to let readers, aggregators and auto-posters watch that content without an account or an API key. This guide builds a complete Tornado RSS feed with a clean query, stable identifiers and correct dates, explains how its built-in ETag handling works and ends with the checks that keep the feed dependable once other systems rely on it.

When a Tornado service needs a feed

A feed is worth adding whenever the service publishes things that people outside it want to follow:

  • articles, news or announcements stored in a database and served by the application;
  • release notes or a changelog for a product, a library or an internal tool;
  • listings, events, offers or openings added by staff or by users;
  • any content that should also appear on social networks without being copied by hand.

If your content already lives in a blog or a CMS, use the feed it provides instead; see how to find your WordPress feed link. If you use another Python framework, the same idea works in Flask dan FastAPI; only the handler style changes.

The complete handler

Tornado needs no installation beyond the package itself, and this example uses only the standard library for everything else: sqlite3 for the data, ElementTree for the XML and the email.utils module for dates. The condition on the publication time matters: without it, an item scheduled for tomorrow would appear in the feed today, and an auto-poster would share it before it exists on the website. Publication times are stored as UTC timestamps.

import asyncio
import os
import sqlite3
from datetime import datetime, timezone
from email.utils import format_datetime
from xml.etree import ElementTree as ET

import tornado.web

SITE_URL = os.environ.get("SITE_URL", "https://www.example.com")
ATOM = "http://www.w3.org/2005/Atom"
ET.register_namespace("atom", ATOM)


def rfc822(timestamp):
    moment = datetime.fromtimestamp(timestamp, tz=timezone.utc)
    return format_datetime(moment, usegmt=True)


def load_rows():
    db = sqlite3.connect("data/site.db")
    try:
        now = int(datetime.now(timezone.utc).timestamp())
        return db.execute(
            "SELECT id, title, slug, summary, published_at FROM posts "
            "WHERE status = 'published' AND published_at <= ? "
            "ORDER BY published_at DESC LIMIT 20",
            (now,),
        ).fetchall()
    finally:
        db.close()


def build_feed(rows):
    rss = ET.Element("rss", version="2.0")
    channel = ET.SubElement(rss, "channel")
    ET.SubElement(channel, "title").text = "Example News"
    ET.SubElement(channel, "link").text = SITE_URL + "/"
    ET.SubElement(channel, "description").text = "The latest posts from Example News"
    ET.SubElement(channel, "language").text = "en"
    newest = rows[0][4] if rows else 0
    ET.SubElement(channel, "lastBuildDate").text = rfc822(newest)
    ET.SubElement(channel, "{%s}link" % ATOM, href=SITE_URL + "/feed.xml",
                  rel="self", type="application/rss+xml")
    for post_id, title, slug, summary, published in rows:
        item = ET.SubElement(channel, "item")
        ET.SubElement(item, "title").text = title
        ET.SubElement(item, "link").text = SITE_URL + "/" + slug + "/"
        ET.SubElement(item, "guid", isPermaLink="false").text = "post-" + str(post_id)
        ET.SubElement(item, "pubDate").text = rfc822(published)
        ET.SubElement(item, "description").text = summary
    return '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(rss, encoding="unicode")


class FeedHandler(tornado.web.RequestHandler):
    async def get(self):
        rows = await asyncio.to_thread(load_rows)
        self.set_header("Content-Type", "application/rss+xml; charset=utf-8")
        self.set_header("Cache-Control", "public, max-age=300")
        self.write(build_feed(rows))


async def main():
    app = tornado.web.Application([(r"/feed.xml", FeedHandler)])
    app.listen(8888)
    await asyncio.Event().wait()


if __name__ == "__main__":
    asyncio.run(main())

Run it with python and open the route in a browser or with curl. You should see well-formed XML with at most twenty items, newest first. The query runs in a worker thread through asyncio.to_thread, so a slow disk never blocks the event loop that serves other requests. With PostgreSQL or MySQL you would use an async driver instead, and the rest of the handler would stay the same.

How the built-in ETag works

When a handler finishes a successful GET or HEAD request and has not set an ETag itself, Tornado hashes the buffered response body, adds the result as the ETag header and compares it with the If-None-Match header of the request. If they match, it discards the body and changes the status to 304. You get conditional requests with no code at all, and the bandwidth saving is real: a poll that finds nothing new costs a query and a few hundred bytes of headers.

The hash is computed from the body, which brings one rule: the body must not change unless the content changes. That is why the example takes the last build date from the newest item and not from the clock. If you wrote the current time there, the hash would change on every request, the ETag would never match and the 304 answer would never be sent. The same rule applies to any generator that stamps the current time into the output, in any language. The background to this technique is in the guide to ETags and conditional GET.

Where each field comes from

Feed fieldSource in the exampleRule to keep
titleThe title columnPlain text, escaped by ElementTree
linkSite address plus slugAbsolute address that returns 200 directly
guidThe row id with a prefixNever changes, even if the slug changes
pubDateThe publication timestamp in UTCRFC 822 format with GMT, never in the future
descriptionThe summary columnOne or two sentences that make sense alone
lastBuildDateThe newest publication timestampChanges only when the content changes

The identifier is the most important decision. An auto-poster remembers which items it has already shared by their GUID. If the value changes when someone edits a slug or republishes an article, the same story is shared again. Using the database id with a fixed prefix, and marking it as not a permanent link, avoids that. The problem and its fixes are described in why a feed auto-posts the same article twice.

Dates in the right format

RSS dates follow RFC 822, with English abbreviations for days and months. The format_datetime function in the standard email.utils module produces exactly that shape, and with the usegmt option it writes GMT as the zone, which every parser accepts. It requires a timezone-aware value in UTC, which is a useful reminder: convert everything to UTC before it reaches the feed. Do not use ISO 8601 in an RSS document; that format belongs to Atom feeds, and many RSS readers reject it. If a validator complains about dates, read the guide to RSS date format errors.

Caching without delaying new posts

Auto-posters, readers and search engines request a feed often, and almost always nothing has changed. The Cache-Control header in the example allows shared caches to keep the response for five minutes, and Tornado answers the rest with 304. Choose the period with care: a long one delays every new post by the same amount, because caches keep serving the old list until it expires. A few minutes is a good balance for most sites. If the table grows large, add an index on the status and publication columns so that the query stays fast. More on this trade-off is in the guide to feed caching.

Running Tornado in production

In production, run several processes under a process manager and put a reverse proxy in front of them that terminates HTTPS. Keep the public address in your own setting rather than trusting forwarded headers, so that links in the feed are always correct. Check that the proxy does not strip the ETag and Cache-Control headers and that it does not add its own long cache that keeps old XML for too long. After every deployment, request the feed once to confirm that it still returns 200 and that the address is exactly the one you gave to the auto-poster. If the feed ever stops updating, this checklist shows what to inspect first.

Images and richer items

An auto-poster is more useful when each item carries a picture. Add an enclosure element to each item with the address, the length in bytes and the type of the image as attributes, and fill it from an image column. Use an absolute https address and a file of reasonable size. Without an image, many networks show a plain text card. How tools read these elements is explained in the guide to enclosure tags. If you want HTML inside the description, read the CDATA guide first.

Test the feed before you connect it

  1. Request the route with curl and look at the headers: the status should be 200, the content type should be application/rss+xml and an Etag header should be present.
  2. Repeat the request with that value in an If-None-Match header and confirm that a 304 comes back.
  3. Paste the public address into the W3C Feed Validation Service and fix every error.
  4. Insert a test row and check that it appears at the top of the feed on the next request.
  5. Set a publication time in the future and check that the item stays hidden until then.

There is a longer checklist in the feed validation guide and a short curl routine in the guide to debugging a feed with curl.

Common mistakes to avoid

  • Blocking the event loop. Run synchronous database calls in a thread or use an async driver.
  • Letting the build date follow the clock. Derive it from the newest item so that the built-in ETag can match.
  • Future items. Filter by publication time so scheduled content stays hidden until its moment.
  • Unstable GUIDs. Use an identifier that never changes.
  • Addresses taken from the request. Build links from the configured public address.
  • Changing the feed address. Keep the route, or redirect the old one permanently.

How PostRSS works with a Tornado feed

PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your feed route, connect your accounts and choose how each post is built from the title, the description or the page title. PostRSS checks the feed as often as every minute and shares each new item, with its image and link, to the channels you choose among 66 supported networks, messengers, team chats and blogs, including LinkedIn, X, Bluesky, Mastodon and Telegram. Keyword filters, UTM parameters and posting windows let you tune each channel, and webhooks connect PostRSS to your own systems. See the PostRSS features page and the plan limits on the pricing page. More developer guides are under the Technical Guide tag.

Bacaan Berkaitan

The bottom line

Tornado needs only a handler, a non-blocking query and the standard library to serve a dependable RSS feed: select published items dated in the past, let ElementTree escape every value, use a stable GUID, format dates in UTC with email.utils, derive the last build date from the newest item and send the result with the right content type. The framework adds the ETag and the 304 answer by itself. Test the route with curl and a validator, keep the address unchanged and connect it to an auto-poster. After that, every item you publish can reach your social channels automatically.

FAQ

Does Tornado have a built-in RSS generator?

No. Tornado has no feed support, but ElementTree from the standard library produces a valid RSS document, and the handler returns it with the right content type.

Do I need to write ETag code in Tornado?

Not for a simple feed. For successful GET and HEAD requests Tornado hashes the body, sets the ETag header and returns 304 when the request carries a matching If-None-Match value. You only need to keep the body stable.

Why does my feed never return 304?

Most often the body changes between requests, for example because the last build date is the current time. Take that date from the newest item and the hash stays the same until the content changes.

How many items should the feed contain?

Between ten and twenty of the newest published items is enough. An auto-poster only needs recent items, and a smaller file is faster to download and parse.

Can I use this feed with PostRSS?

Yes. PostRSS reads any valid RSS or Atom address that is reachable without a login. Paste the address, connect your channels and new items are shared as soon as they appear.

New guides, once a month

What changed in the networks, what broke, and how to fix it before it costs you reach.

We send a confirmation e-mail first. Unsubscribe any time.

Lebih banyak alat daripada pasukan kami

Dibina oleh Internet Solutions, pasukan di sebalik PostRSS. Setiap produk menjimatkan masa anda dengan cara tersendiri.