PodcastFeedVapor

Publié le · Mis à jour le · 18 min

Wlad
Wlad
Fondateur & Tech Lead Swift

D'où ça vient

PodcastFeedMaker sait générer, parser, valider et auditer des flux RSS podcast. Super. Mais une fois que vous avez votre XML tout propre… il faut bien le servir quelque part.

Dans un projet client, j'avais besoin de feeds générés à la volée depuis une base de données — pas de fichiers statiques, pas de CDN, du vrai RSS construit au moment de la requête HTTP. J'ai commencé par écrire un handler Vapor naïf : construire le feed, sérialiser, retourner la réponse. Ça marchait, mais sans ETag, sans Cache-Control, sans CORS, sans rien de ce qu'un flux RSS de production exige vraiment.

Plutôt que de réinventer ces briques à chaque projet, j'ai tout extrait dans une lib à part. PodcastFeedVapor est née de ce besoin concret : transformer n'importe quelle app Vapor en serveur de podcasts, avec tout ce qui va bien côté HTTP, tout en gardant une séparation stricte entre ce qui est réutilisable (la lib open-source) et ce qui est métier (votre app).

Ce que fait PodcastFeedVapor

C'est une couche middleware pour Vapor qui s'intercale entre votre logique métier et le client HTTP. En gros : vous construisez un PodcastFeed avec le DSL de PodcastFeedMaker, et PodcastFeedVapor se charge du reste — les headers, le caching, le CORS, le streaming, la pagination.

Le tout est découpé en quatre targets Swift Package Manager. Vous importez uniquement ce dont vous avez besoin, pas plus.

  • Middleware HTTP — Trois middlewares empilables : header X-Generator, ETag/304 avec SHA256, et CORS avec preflight OPTIONS

  • Route builder — Un DSL qui enregistre des routes feed en une ligne : app.podcastFeed("feed.xml") { req in ... }

  • Streaming XML — Génération chunked via StreamingFeedResponse pour les catalogues de 10 000+ épisodes sans exploser la RAM

  • Pagination — Parsing automatique de ?limit=N&offset=N avec clamping de sécurité pour éviter les abus

  • Podping — Notifications webhook quand un feed change, pour prévenir les agrégateurs en temps réel

  • Audit par lot — Scoring qualité en parallèle sur plusieurs feeds d'un coup, avec notes et recommandations

  • Mapping Fluent — Trois protocoles Swift purs pour convertir vos modèles en feeds sans dépendance Fluent dans la lib

  • Cache Redis — Target optionnel avec FeedCacheStore et implémentation Redis (SETEX + SCAN)

  • Queue workers — Target optionnel pour régénérer des feeds en arrière-plan via Vapor Queues

  • Streaming cache — Caching stream-through via StreamingCacheResponse : le XML est caché pendant qu'il est streamé au client, avec ETag automatique sur les cache hits

  • Métriques — Middleware FeedMetricsMiddleware compatible swift-metrics : compteurs de requêtes, timers de latence, recorders de taille de réponse — branchez Prometheus, StatsD ou Datadog

  • WebSocket Podping — Notifications en temps réel via WebSocket avec PodpingWebSocketManager : connexion, souscription par feed, broadcast filtré

  • Strict concurrency — Tous les types sont Sendable, Swift 6.2 strict concurrency de bout en bout

Servir un feed en 10 lignes

Voici tout ce qu'il faut pour qu'une app Vapor serve un flux RSS podcast avec caching HTTP, CORS, et un header de génération personnalisé. Pas de magie, juste de la config :

Un GET /feed.xml retourne du XML avec Content-Type: application/rss+xml, un ETag SHA256, un Cache-Control: public, max-age=3600, un Last-Modified, et un header X-Generator: MyApp/1.0. Le tout en 200 OK la première fois, puis 304 Not Modified ensuite si le contenu n'a pas changé. Exactement ce qu'on attend d'un feed en prod.

Les trois middlewares

Trois middlewares couvrent les besoins HTTP classiques d'un flux RSS en production. Ils s'empilent dans un ordre précis — CORS en premier (le plus externe), puis le cache, puis le header de génération (le plus interne). Chacun ne touche que les réponses RSS et ignore silencieusement le reste. Votre API JSON continue de fonctionner normalement.

PodcastFeedMiddleware

Ajoute un header X-Generator configurable sur chaque réponse RSS. C'est utile pour le debugging, le monitoring, et surtout pour identifier quel serveur a produit un feed dans un environnement distribué. Les réponses non-RSS (JSON, HTML, etc.) passent sans modification — le middleware détecte le Content-Type et n'intervient que sur application/rss+xml.

FeedCacheMiddleware

Gère le caching HTTP conditionnel. À chaque réponse RSS, il calcule un hash SHA256 du body et l'attache comme ETag. Si le client renvoie ce tag dans un If-None-Match, et que le contenu n'a pas changé, le middleware court-circuite la réponse avec un 304 Not Modified — zéro body, zéro travail de génération côté serveur.

Il ajoute aussi un Last-Modified (date de génération) et un Cache-Control: public, max-age=N configurable via CacheControlDuration — en minutes, heures, ou secondes selon vos besoins.

CORSFeedMiddleware

CORS complet avec gestion du preflight OPTIONS. Par défaut, il autorise toutes les origines (*) avec un max-age de 86400 secondes. Vous pouvez restreindre à une origine spécifique si votre cas d'usage l'exige. Le preflight retourne un 204 No Content sans body — exactement ce qu'attend un navigateur moderne.

Headers produits

HeaderMiddlewareExemple

X-Generator

PodcastFeedMiddleware

MyApp/1.0

ETag

FeedCacheMiddleware

"41293260aba6..." (SHA256)

Last-Modified

FeedCacheMiddleware

Sat, 15 Feb 2026 14:30:00 GMT

Cache-Control

FeedCacheMiddleware

public, max-age=300

Access-Control-Allow-Origin

CORSFeedMiddleware

*

Access-Control-Max-Age

CORSFeedMiddleware

86400

Le route builder

podcastFeed() est une extension sur Application et RoutesBuilder qui enregistre un GET avec encodage automatique du PodcastFeed en XML. Ça supporte les chemins statiques et les paramètres dynamiques, exactement comme les routes Vapor que vous connaissez déjà.

En interne, FeedResponseEncoder convertit le PodcastFeed en Response avec le bon Content-Type, le pretty-print si configuré, et passe le relais aux middlewares pour les headers HTTP. Si vous préférez garder le contrôle total, req.feedResponse(feed) et Response.xml("...") sont aussi disponibles.

Pagination et gros catalogues

Pour un podcast avec 500 épisodes, servir les 500 à chaque requête n'a aucun sens. FeedPagination extrait les paramètres limit et offset de la query string et les clampe dans des bornes raisonnables. Pas de limite à 10 000 par erreur, pas d'offset négatif qui ferait planter votre requête SQL.

Pour les catalogues vraiment massifs où même la pagination ne suffit pas, StreamingFeedResponse génère le XML par chunks et le streame en Transfer-Encoding: chunked — le serveur n'a jamais besoin de garder le feed entier en mémoire. Parfait pour les plateformes qui hébergent des milliers d'épisodes.

Mapping depuis vos modèles

Trois protocoles Swift purs transforment vos types en feeds. Attention : ce ne sont pas des protocoles Fluent — ils marchent avec n'importe quel type, struct ou class. C'est juste un contrat simple : vous implémentez une méthode to*(), et la lib sait convertir.

ProtocoleConvertit versUsage

FeedMappable

PodcastFeed

Le modèle principal (show → feed complet)

ChannelMappable

Channel

Les métadonnées du show

ItemMappable

Item

Un épisode individuel

Podping et audit par lot

Quand vous publiez un épisode, les agrégateurs ne vérifient pas votre feed en continu — ils attendent un signal. Podping est ce signal. PodpingNotifier envoie un webhook HTTP POST pour prévenir l'écosystème qu'un feed a changé. Simple, efficace.

L'audit par lot expose un endpoint qui score plusieurs feeds en parallèle via le FeedAuditor de PodcastFeedMaker. Il suffit de passer les URLs en query string — chaque feed est récupéré, parsé, analysé, et noté. Pratique pour monitorer la qualité d'un catalogue entier.

La réponse JSON contient le score (0-100), la note lettrée (A à F), et le nombre de recommandations pour chaque feed. On a testé avec de vrais feeds en production pendant le développement — un Simplecast a obtenu un D/68 avec 9 recommandations, un Megaphone qui renvoyait du HTML au lieu du RSS a logiquement pris un F/0. La réalité du terrain.

Redis et Queues — les targets optionnels

Le core de PodcastFeedVapor n'a aucune dépendance sur Redis ou sur Vapor Queues. Deux targets optionnels ajoutent ces capacités pour les déploiements qui en ont vraiment besoin.

PodcastFeedVaporRedis

FeedCacheStore définit un contrat simple : stocker, récupérer, invalider. L'implémentation Redis utilise SETEX (set with expiry) et SCAN pour l'invalidation par pattern. Le tout se branche en une ligne dans votre config :

PodcastFeedVaporQueues

Pour les feeds lourds ou les régénérations planifiées, FeedRegenerationJob s'intègre dans Vapor Queues. Vous implémentez un handler, vous l'enregistrez, et vous pouvez dispatcher des jobs de régénération depuis n'importe quel handler de route.

Streaming cache

Le streaming XML de la 0.1.0 résolvait le problème mémoire sur les gros catalogues. Mais chaque requête regénérait le feed from scratch. StreamingCacheResponse combine les deux : le XML est streamé au client ET caché simultanément. La première requête génère et stocke, les suivantes sont servies depuis le cache avec un ETag SHA256.

FeedCacheStore est un protocole — vous branchez le backend que vous voulez. InMemoryFeedCache est fourni pour le dev et les tests, RedisFeedCache pour la production.

Première requête : Transfer-Encoding: chunked, pas d'ETag — le XML est généré et streamé en temps réel. Deuxième requête : Content-Length fixe, ETag SHA256, servi depuis le cache. Le client peut ensuite envoyer un If-None-Match pour obtenir un 304.

Métriques

Le target PodcastFeedVaporMetrics s'appuie sur swift-metrics d'Apple — backend-agnostique, vous branchez Prometheus, StatsD, Datadog ou n'importe quel collecteur compatible. FeedMetricsMiddleware s'ajoute en premier dans la stack middleware et enregistre automatiquement chaque requête feed.

MétriqueTypeDescription

pfv_feed_requests_total

Counter

Nombre de requêtes par route, status, cache hit/miss

pfv_feed_request_duration_seconds

Timer

Latence par route et status

pfv_feed_response_size_bytes

Recorder

Taille des réponses par route

pfv_feed_active_streams

Gauge

Connexions streaming actives en temps réel

Le préfixe pfv_ est configurable via FeedMetricsConfiguration. Le gauge des streams actifs est un actor dédié (FeedActiveStreamsGauge) qui s'incrémente quand un stream démarre et se décrémente à la fin — pas de lock, pas de race condition.

WebSocket Podping

La 0.1.0 avait PodpingNotifier pour les webhooks HTTP sortants. La 0.2.0 ajoute un canal WebSocket entrant : les clients se connectent, s'abonnent à des feeds spécifiques, et reçoivent des notifications push quand un feed est mis à jour. Les deux mécanismes coexistent — webhook pour notifier les agrégateurs, WebSocket pour notifier vos propres clients.

Côté client, la communication se fait en JSON avec 5 types de messages : welcome à la connexion, subscribe/unsubscribe pour filtrer, subscribed en confirmation, et notification pour les mises à jour. Un client sans filtre reçoit tout ; un client abonné ne reçoit que les feeds qui l'intéressent.

Le sample server

Pendant le développement, on a construit un serveur de test complet pour valider chaque fonctionnalité avec de vrais appels HTTP. Le repo inclut un script qui le génère automatiquement comme projet Vapor autonome — pratique pour voir comment tout s'assemble.

Le serveur expose 11 endpoints qui couvrent toutes les fonctionnalités de la lib : feed statique, feed riche avec métadonnées Podcast NS 2.0, feed dynamique avec pagination, audit par lot, health check, CORS preflight, streaming cache avec ETag, WebSocket Podping en temps réel, et métriques swift-metrics.

MéthodeRouteCe qu'elle fait

GET

/health

Health check JSON (status, version, uptime)

GET

/feed.xml

Feed statique — 3 épisodes

GET

/rich/feed.xml

Feed riche — métadonnées complètes, enclosures, Podcast NS 2.0

GET

/shows/:showId/feed.xml

Feed dynamique avec paramètre de route

GET

/shows/:showId/feed.xml?limit=N&offset=N

Feed dynamique avec pagination

GET

/feeds/audit?urls=...

Audit par lot — scoring qualité en parallèle

OPTIONS

/feed.xml

CORS preflight — 204 No Content

GET

/api/status

Endpoint JSON — vérifie le skip middleware

GET

/cached/feed.xml

Feed caché avec StreamingCacheResponse (stream-through + ETag)

WS

/podping

WebSocket Podping — notifications temps réel

POST

/notify?url=...&reason=...

Déclenche un broadcast vers les clients WebSocket

24 scénarios validés en live

Chaque fonctionnalité a été testée avec curl sur un vrai serveur Vapor tournant en local. 24 scénarios au total — 12 vérifications statiques et 12 tests fonctionnels live incluant streaming cache, WebSocket Podping et métriques. Quand on dit que ça marche, c'est parce qu'on l'a vraiment fait tourner.

Pourquoi deux fichiers Swift

Un détail d'architecture qui mérite d'être mentionné : dans le sample server, la construction des feeds (Feeds.swift) est séparée du point d'entrée Vapor (App.swift). Pourquoi ? Un conflit de noms entre le Channel de PodcastFeedMaker et le Channel de NIO (le runtime réseau de Vapor). En gardant les imports séparés, le compilateur résout les types sans ambiguïté. C'est documenté dans le projet parce que tout développeur qui mélange les deux imports tombera dessus tôt ou tard.

Architecture

Tests

282 tests répartis dans 55 suites sur 4 targets. Couverture globale de 93.85% — en hausse de +3.6% par rapport à la 0.1.0. Le core monte à 99%+, les queues à 98.1%. Redis est plus bas (27.3%) parce que les tests d'intégration nécessitent un serveur Redis réel, mais le protocole FeedCacheStore est entièrement testé via un mock.

Chaque feature a ses showcase tests qui servent aussi de documentation exécutable. Les exemples de code dans le DocC et dans cet article sont tirés de ces tests — ce qui est écrit ici a été compilé et exécuté.

Installation

Prérequis

  • Swift 6.2+ avec strict concurrency

  • Vapor 4.121+

  • PodcastFeedMaker 0.2.0+

  • Plateformes : macOS 14+ · Linux (Ubuntu 22.04+)

Swift Package Manager

Quatre produits sont disponibles. Importez le core, et ajoutez Redis, Queues ou Metrics uniquement si votre déploiement les utilise vraiment :

Références

Les specs sur lesquelles s'appuie PodcastFeedVapor, via PodcastFeedMaker :

Et après

  • Redis + streaming cache distribué — Combiner RedisFeedCache avec StreamingCacheResponse pour un cache partagé entre instances sur les déploiements multi-nœuds (Kubernetes, Docker Swarm)

  • Authentication WebSocket — Support de tokens d'authentification sur les connexions Podping WebSocket pour sécuriser les environnements où les feeds ne sont pas publics

  • Dashboard Grafana — Un JSON importable avec les 4 métriques pfv_* prêt à brancher sur n'importe quel déploiement Prometheus

Sous le capot

  • Swift 6.2 — Strict concurrency, tous les types Sendable, actors partout

  • 4 targets modulaires — Core, Redis, Queues, Metrics — importez ce dont vous avez besoin

  • 282 tests — 55 suites, couverture 93.85%

  • macOS + Linux — CI sur les deux plateformes

  • 8+ guides DocC — Documentation complète avec exemples exécutables

Liens

GitHub - atelier-socle/podcast-feed-maker-vapor: Vapor middleware for serving podcast RSS feeds with caching, streaming, Podping, and Fluent integration. Built on PodcastFeedMaker.

GitHub - atelier-socle/podcast-feed-maker-vapor: Vapor middleware for serving podcast RSS feeds with caching, streaming, Podping, and Fluent integration. Built on PodcastFeedMaker.

Vapor middleware for serving podcast RSS feeds with caching, streaming, Podping, and Fluent integration. Built on PodcastFeedMaker. - atelier-socle/po…

GitHub