
Short answer: To serve an RSS feed from a Vert.x application, register a GET route for /feed.xml on a Router, load the newest published rows through a non-blocking client so the event loop never waits, write the channel and items with a small builder that escapes every value, format dates in UTC with DateTimeFormatter.RFC_1123_DATE_TIME, and send the bytes as application/rss+xml. Hash the body into an ETag and answer a matching If-None-Match header with a 304, take the last build date from the newest item so the body stays identical between changes, test the route with curl and a validator, then give the public address to an auto-poster.
Vert.x is the toolkit for building reactive applications on the Java virtual machine. It is small, fast and unopinionated, and teams use it for APIs, gateways, real-time services and content backends that must handle many connections with few threads. When such a service publishes something that people want to follow, such as articles, offers, events or release notes, a feed is the standard way to let readers, aggregators and auto-posters watch it without an account or an API key. Vert.x Web has no feed support, but the Java platform has everything needed to write a valid RSS document. This guide builds a complete Vert.x RSS feed with a router, a non-blocking query, stable identifiers, correct dates and conditional requests, explains each decision and ends with the checks that keep the feed dependable once other systems rely on it.
A feed is worth adding whenever new items appear on a schedule you do not control: a changelog that grows with every release, a directory of listings, a news section or a library of guides. Readers who follow it in a feed reader get every item in order, search engines discover new pages faster and an auto-poster such as PostRSS can share each new item on social networks as soon as it is published.
If your content already lives in a blog or a CMS, use the feed that system provides; see how to find your WordPress feed link. If you prefer a feed library and a bigger framework, the Spring Boot RSS feed guide shows the Rome approach, and the Javalin RSS feed guide covers another small server. Build the route yourself when the content is in your own database and no ready-made feed exists.
The example needs Java 17 or newer, because it uses records and HexFormat, and the vertx-web module. With Maven, add the dependency and take the version from the current Vert.x 4 release line.
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
</dependency>Assume a repository class that returns the newest published articles as a Future. How it fetches them is up to you: a reactive SQL client, a pool wrapped around JDBC or a call to another service all work, as long as nothing blocks the event loop. The code below depends only on the interface, so the feed logic stays easy to test.
The first file holds the data shape and the feed writer. The record keeps one article, and the writer builds the XML in a single pass with a helper that escapes the five characters that XML reserves. The publication time is stored as a UTC unix timestamp, and the repository only returns items whose time has arrived, so an item scheduled for tomorrow never reaches the feed today.
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.List;
public final class FeedWriter {
public record Article(String slug, String title, String summary,
String imageUrl, long imageBytes, String imageType,
long publishedAt) {}
private static final String SITE = "https://example.com";
static String rfc822(long epochSeconds) {
return DateTimeFormatter.RFC_1123_DATE_TIME.format(
Instant.ofEpochSecond(epochSeconds).atOffset(ZoneOffset.UTC));
}
static String esc(String value) {
return value.replace("&", "&").replace("<", "<")
.replace(">", ">").replace("\"", """)
.replace("'", "'");
}
public static byte[] write(List<Article> items) {
long newest = items.isEmpty() ? 0 : items.get(0).publishedAt();
StringBuilder xml = new StringBuilder(8192);
xml.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.append("<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n");
xml.append("<channel>\n");
xml.append("<title>Example Journal</title>\n");
xml.append("<link>").append(SITE).append("/</link>\n");
xml.append("<description>News and guides from Example Journal.</description>\n");
xml.append("<language>en</language>\n");
xml.append("<lastBuildDate>").append(rfc822(newest)).append("</lastBuildDate>\n");
xml.append("<atom:link href=\"").append(SITE)
.append("/feed.xml\" rel=\"self\" type=\"application/rss+xml\"/>\n");
for (Article a : items) {
String url = SITE + "/articles/" + a.slug() + "/";
xml.append("<item>\n");
xml.append("<title>").append(esc(a.title())).append("</title>\n");
xml.append("<link>").append(esc(url)).append("</link>\n");
xml.append("<guid isPermaLink=\"true\">").append(esc(url)).append("</guid>\n");
xml.append("<pubDate>").append(rfc822(a.publishedAt())).append("</pubDate>\n");
xml.append("<description>").append(esc(a.summary())).append("</description>\n");
if (a.imageUrl() != null) {
xml.append("<enclosure url=\"").append(esc(a.imageUrl()))
.append("\" length=\"").append(a.imageBytes())
.append("\" type=\"").append(esc(a.imageType())).append("\"/>\n");
}
xml.append("</item>\n");
}
xml.append("</channel>\n</rss>\n");
return xml.toString().getBytes(StandardCharsets.UTF_8);
}
}The second file holds the verticle that starts the server and the route. It computes the validator from the finished bytes and answers 304 when the request already holds the current version.
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
import io.vertx.ext.web.Router;
import java.security.MessageDigest;
import java.util.HexFormat;
public class FeedVerticle extends AbstractVerticle {
private final ArticleRepository repository;
public FeedVerticle(ArticleRepository repository) {
this.repository = repository;
}
@Override
public void start(Promise<Void> startPromise) {
Router router = Router.router(vertx);
router.get("/feed.xml").handler(ctx ->
repository.latest(20).onSuccess(items -> {
byte[] body = FeedWriter.write(items);
String etag = "\"" + sha256(body).substring(0, 32) + "\"";
var response = ctx.response()
.putHeader("ETag", etag)
.putHeader("Cache-Control", "public, max-age=300");
if (matches(ctx.request().getHeader("If-None-Match"), etag)) {
response.setStatusCode(304).end();
return;
}
response
.putHeader("Content-Type", "application/rss+xml; charset=utf-8")
.end(Buffer.buffer(body));
}).onFailure(ctx::fail));
vertx.createHttpServer()
.requestHandler(router)
.listen(8080)
.onSuccess(server -> startPromise.complete())
.onFailure(startPromise::fail);
}
private static String sha256(byte[] body) {
try {
byte[] digest = MessageDigest.getInstance("SHA-256").digest(body);
return HexFormat.of().formatHex(digest);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
private static boolean matches(String header, String etag) {
if (header == null) return false;
for (String token : header.split(",")) {
String t = token.trim();
if (t.startsWith("W/")) t = t.substring(2);
if (t.equals("*") || t.equals(etag)) return true;
}
return false;
}
}Deploy the verticle from your main method with vertx.deployVerticle(new FeedVerticle(repository)). The router answers GET requests for the route, and a HEAD request can be allowed by adding a second route for the head method if your monitoring tools use it.
A Vert.x event loop thread serves many connections. If a handler waits for a slow query on that thread, every connection assigned to the loop waits with it, and the whole service seems to freeze. The repository in this example returns a Future, which means the work is started and the handler continues when the result arrives. A reactive SQL client does this natively. A plain JDBC connection must be wrapped with the blocking helper, which moves the call to a worker thread. Keep the query small: twenty rows, one table, an index on the publication column.
Auto-posters, readers and search engines request a feed often, and almost always nothing has changed. The handler hashes the finished bytes, quotes the hash as an entity tag and compares it with the If-None-Match header. A match produces a 304 response with no body, which saves bandwidth for both sides. The hash is computed from the body, which brings one rule: the body must not change unless the content changes. That is why the writer takes the last build date from the newest item and not from the clock. If you wrote the current time into that element, the hash would change on every request and no client would ever receive a 304. A compressing proxy may turn a strong tag into a weak one, so the comparison strips the W/ prefix.
| Feed element | Source | Rule |
|---|---|---|
| title | Article title | Plain text, escaped by the helper |
| link | Public address of the page | Absolute, with https, returning 200 directly |
| guid | Same address, or a stored identifier | Never changes after publication |
| pubDate | Publication timestamp | RFC 822 in UTC, never the current time |
| description | Short summary | One or two sentences of plain text |
| enclosure | Featured image | Address, size in bytes and media type |
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. If you may rename slugs, store a separate permanent identifier and write it with isPermaLink set to false.
RSS dates follow RFC 822, with English abbreviations for days and months. The formatter named RFC_1123_DATE_TIME produces that style, always with English names regardless of the server locale, and it writes the zone as GMT when the offset is zero. It does not pad a single-digit day with a zero, which the format allows. The timestamp comes from the database as a number, is converted to an offset date-time in UTC and is never formatted with the local zone of the server. Mixing in local time is the most common cause of feeds whose items appear hours late or early in readers.
The Cache-Control header in the example lets shared caches keep the response for five minutes, and the ETag lets everyone revalidate cheaply after that. If you place a content delivery network in front of the application, keep the lifetime short so a new item does not wait long for its turn. A feed that lags behind the website by an hour defeats the purpose of automation. For the background, read how auto-posting tools use ETags and conditional requests और what 301, 304, 404 and 500 mean for auto-posting.
A few minutes of testing prevents days of confusion later. Start the service and check it from the outside:
curl -i https://example.com/feed.xml and confirm a 200 status, the right content type and an ETag header.-H 'If-None-Match: ...' and the tag you received, and confirm that the answer is 304.There is a longer checklist in the feed validation guide and a short curl routine in the guide to debugging a feed with curl. Because the writer is a plain function, it is also easy to cover with a unit test that parses its output.
Run the application behind a reverse proxy that terminates HTTPS and passes the real host and scheme, so absolute links are correct. Keep the site address in configuration instead of a constant, so a staging copy never publishes production links. Deploy several instances of the verticle if your hardware has several cores, and monitor the feed route separately from the home page, because a feed can fail while the rest of the site works. If the route sits behind a login or a firewall rule for unknown clients, an auto-poster will not reach it; the feed must be public.
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.
Vert.x needs only a router, a non-blocking query and a small escaping writer to serve a dependable RSS feed: select published items dated in the past, escape every value, use a stable GUID, format dates in UTC, derive the last build date from the newest item and send the result with the right content type. Add a hash-based ETag so repeat requests end in a 304. 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.
No. Vert.x Web has no feed support, but a short writer class that escapes values and formats dates produces a valid RSS document, and the route returns it with the right content type.
An event loop thread serves many connections, so a slow blocking call stalls all of them. Use a reactive client or move blocking work to a worker thread.
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.
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.
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.
What changed in the networks, what broke, and how to fix it before it costs you reach.
PostRSS बनाने वाली टीम Internet Solutions की ओर से। हर प्रोडक्ट आपका समय अपने तरीके से बचाता है।