Se rendre au contenu

Controllers HTTP et API REST en Odoo 19 : http.Controller, @http.route et modes d'authentification

Bloc 5 · Article 4/4 — Clôture du Parcours Fondamentaux Controllers HTTP et API REST en Odoo 19 Exposer ton module au monde extérieur — http.Controller , @http.
26 avril 2026 par
Controllers HTTP et API REST en Odoo 19 : http.Controller, @http.route et modes d'authentification
B.Mustapha

Bloc 5 · Article 4/4 — Clôture du Parcours Fondamentaux

Controllers HTTP et API REST en Odoo 19

Exposer ton module au monde extérieur — http.Controller, @http.route, type='http' vs type='json', modes d'authentification, CSRF et CORS. Deux cas pratiques : une API REST tickets et une page publique de suivi client.

~15 minutes de lecture · article de clôture du Parcours Fondamentaux

Page publique /helpdesk/status/HLP/2026/00006 rendue avec le layout website
Objectif — cette page publique accessible à l'URL /helpdesk/status/<ref> sans authentification, rendue avec le website.layout natif. Le client colle sa référence, voit le statut de son ticket, la date d'ouverture, la catégorie, la résolution.

Ce que tu vas apprendre

http.Controller

La classe qui expose du code Python à HTTP.

@http.route

Le décorateur qui mappe URL → méthode.

type='http' vs 'json'

Page HTML rendue ou endpoint JSON-RPC.

Auth & sécurité

public, user, bearer, csrf=False pour API externes.

Prérequis

  • Module odooskills_helpdesk v19.0.1.14.0 (T22).
  • Module website installé (pour le layout public).
  • Notions QWeb (T20).

1. Le squelette d'un controller

Un controller vit dans controllers/ à la racine du module et s'importe depuis le __init__.py :

# odooskills_helpdesk/__init__.py
from . import models
from . import controllers

# odooskills_helpdesk/controllers/__init__.py
from . import main

# odooskills_helpdesk/controllers/main.py
from odoo import http
from odoo.http import request


class HelpdeskController(http.Controller):

    @http.route('/ma/route', type='http', auth='public')
    def ma_methode(self, **kwargs):
        return "Hello, world !"

Trois éléments-clés :

  • class HelpdeskController(http.Controller) — une classe pour regrouper toutes tes routes. Pas de _name ni d'ORM.
  • @http.route('/ma/route', ...) — le décorateur qui déclare l'URL, le type, l'authentification.
  • request — l'objet global qui donne accès à request.env, request.params, request.httprequest, request.session.

2. type='http' vs type='json'

type='http' Form-encoded / multipart Renvoie HTML via request.render() Pages publiques, formulaires, PDFs csrf=True par défaut type='json' Content-Type: application/json Renvoie dict auto-sérialisé JSON-RPC APIs, intégrations, webhook csrf=False par défaut Les deux types de routes

type='http' — page rendue

@http.route('/helpdesk/status/<path:ref>', type='http', auth='public', website=True)
def helpdesk_status_page(self, ref, **kwargs):
    """Page publique de tracking d'un ticket."""
    ticket = request.env['helpdesk.ticket'].sudo().search(
        [('reference', '=', ref)], limit=1,
    )
    values = {'ref': ref, 'ticket': ticket}
    return request.render('odooskills_helpdesk.ticket_status_page', values)

Points clés :

  • <path:ref>converter qui accepte plusieurs segments séparés par /. Obligatoire ici : les références sont de la forme HLP/2026/00006.
  • website=True — rend la route compatible avec le framework website (menu public, thème, multi-website). Sans ça, pas d'accès à website.layout.
  • .sudo() sur la recherche — auth=public = utilisateur anonyme sans ACL sur helpdesk.ticket. On bypasse pour afficher le ticket, mais on limite les champs exposés côté template.
  • request.render(template_xmlid, values) — rend un template QWeb avec le dict values comme contexte.

type='json' — API REST

@http.route('/api/v1/tickets', type='json', auth='user', methods=['POST'])
def api_tickets(self, **kwargs):
    """Lister ou créer un ticket."""
    action = kwargs.get('action', 'list')
    Ticket = request.env['helpdesk.ticket']

    if action == 'list':
        domain = kwargs.get('domain') or []
        limit = min(int(kwargs.get('limit', 20)), 100)
        tickets = Ticket.search(domain, limit=limit, order='create_date desc')
        return {
            'count': len(tickets),
            'tickets': [{
                'id': t.id,
                'reference': t.reference,
                'name': t.name,
                'state': t.state,
                'partner': t.partner_id.name,
            } for t in tickets],
        }

    if action == 'create':
        ticket = Ticket.create({
            'name': kwargs['name'],
            'partner_id': int(kwargs['partner_id']),
            'channel': kwargs.get('channel', 'email'),
        })
        return {'id': ticket.id, 'reference': ticket.reference}

    return {'error': f"unknown action '{action}'"}

Trois différences structurelles :

  • Pas d'ensure_one ou de template — on retourne un dict, Odoo le sérialise en JSON.
  • methods=['POST'] — conventions REST : list et create en POST dans l'enveloppe JSON-RPC Odoo, pas de GET pour modifier.
  • Les paramètres arrivent dans kwargs — Odoo parse automatiquement le body JSON et le dispatche en arguments nommés.

3. Les modes d'authentification

auth=Qui peut appeler ?Cas d'usage
public Tout le monde, y compris les anonymes. request.env pointe vers l'utilisateur public. Pages publiques website, page de tracking, webhook entrant.
user Utilisateur connecté uniquement. 403 sinon. Endpoints backend réservés aux collaborateurs (ex : portail client connecté, API interne).
none Aucune auth, pas de session Odoo initialisée, pas d'env ORM. Health check, redirections, endpoints purement techniques.
bearer Token API passé dans Authorization: Bearer <token>. APIs tierces sans session cookie. Tokens gérés via Paramètres > Utilisateurs > API Keys.
Nouveauté v17+ — le mode bearer simplifie radicalement l'authentification d'APIs externes. L'admin crée un token côté Paramètres > Utilisateurs > API Keys, le client envoie ce token en header, Odoo connecte la session avec les droits de l'utilisateur propriétaire du token. Plus besoin de hacker la session_id ou de stocker login/password.

4. CSRF — le piège des formulaires publics POST

Odoo active par défaut une protection CSRF sur les routes type='http' en POST. Concrètement, tout formulaire qui appelle ta route doit contenir un champ caché csrf_token.

<form action="/ma/route/post" method="POST">
    <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
    <input type="text" name="nom"/>
    <button type="submit">Envoyer</button>
</form>

Sans ça, Odoo renvoie 403 Forbidden — Session expired.

Désactiver CSRF pour une API externe

Un webhook extérieur (Slack, Stripe, GitHub) ne peut PAS envoyer le CSRF token. On le désactive explicitement :

@http.route('/webhook/stripe', type='http', auth='none', methods=['POST'], csrf=False)
def stripe_webhook(self, **post):
    # Vérifier la signature Stripe manuellement ici
    signature = request.httprequest.headers.get('Stripe-Signature')
    # ...
    return 'ok'
Sécuritécsrf=False retire une couche de défense. Compense-la : vérifier une signature (webhook tiers), limiter par IP (request.httprequest.remote_addr), ou imposer un header secret custom.

Les routes type='json' ont csrf=False par défaut — le format JSON-RPC inclut l'ID de requête comme protection alternative.

5. Tester l'API en ligne de commande

Deux étapes : authentification (obtenir la session cookie) puis appel de l'endpoint.

# 1. Authentification — récupère un cookie de session
curl -c cookies.txt -H "Content-Type: application/json" \
     -d '{
       "jsonrpc": "2.0",
       "params": {
         "db": "vs19_odooskills_test",
         "login": "iastagenie@gmail.com",
         "password": "St@odoodev"
       }
     }' \
     http://localhost:9019/web/session/authenticate

# 2. Appel de l'API — envoie le cookie
curl -b cookies.txt -H "Content-Type: application/json" \
     -d '{
       "jsonrpc": "2.0",
       "params": {"action": "list", "limit": 3}
     }' \
     http://localhost:9019/api/v1/tickets

Réponse typique :

{
  "jsonrpc": "2.0",
  "id": null,
  "result": {
    "count": 3,
    "tickets": [
      {
        "id": 9,
        "reference": "HLP/2026/00009",
        "name": "Test deadline tomorrow",
        "state": "new",
        "priority": "1",
        "partner": "Cabinet Medina",
        "create_date": "2026-04-13T05:44:35"
      },
      {
        "id": 8,
        "reference": "HLP/2026/00008",
        "name": "VIP auto priority",
        "state": "new",
        "priority": "2",
        "partner": "InfoSphere SARL",
        "create_date": "2026-04-13T05:44:35"
      }
    ]
  }
}
Enveloppe JSON-RPC — noter le "result" qui enveloppe ta donnée. Odoo suit le protocole JSON-RPC 2.0 : les erreurs sérialisées reviennent sous "error" au même niveau, avec code, message, data.debug (traceback en dev).

Créer un ticket via l'API

curl -b cookies.txt -H "Content-Type: application/json" \
     -d '{
       "jsonrpc": "2.0",
       "params": {
         "action": "create",
         "name": "Bug signé via API",
         "partner_id": 8,
         "channel": "email",
         "description": "Posté depuis un script externe"
       }
     }' \
     http://localhost:9019/api/v1/tickets

# -> {"result": {"id": 10, "reference": "HLP/2026/00010", "state": "new"}}

6. Page publique — le template QWeb côté website

Côté vue, on crée un template qui enveloppe le contenu dans website.layout :

<template id="ticket_status_page" name="Helpdesk — Suivi de ticket">
    <t t-call="website.layout">
        <div id="wrap" class="oe_structure">
            <div class="container pt-5 pb-5">
                <div class="row">
                    <div class="col-lg-8 mx-auto">

                        <t t-if="not ticket">
                            <div class="alert alert-warning">
                                <h3>Ticket introuvable</h3>
                                <p>Aucun ticket ne correspond à <strong><t t-out="ref"/></strong>.</p>
                            </div>
                        </t>

                        <t t-else="">
                            <h1>Suivi du ticket <t t-out="ticket.reference"/></h1>
                            <div class="card shadow-sm">
                                <div class="card-body">
                                    <h4><t t-out="ticket.name"/></h4>
                                    <!-- ...contenu... -->
                                </div>
                            </div>
                        </t>

                    </div>
                </div>
            </div>
        </div>
    </t>
</template>
Page publique avec ticket en statut Nouveau
Même template, autre état : ticket nouvellement créé. Le t-if/t-else gère les trois scénarios (ticket résolu, en cours, introuvable) sans dupliquer le layout.
Page publique avec message d'erreur 'Ticket introuvable'
Cas d'erreur : référence inconnue. Alerte Bootstrap warning, pas de fuite d'info, le client sait que l'URL est mauvaise.

Trois détails qui comptent :

  • t-call="website.layout" — enveloppe automatique avec header/footer/menu/logo du site. Ton contenu hérite du thème actif.
  • <div id="wrap" class="oe_structure"> — zone éditable depuis le builder website (admin peut ajouter des snippets autour de ta page).
  • Bootstrap 5 + FontAwesome dispos — pas besoin de charger d'assets supplémentaires, ils sont déjà dans web.assets_frontend.

Les pièges à connaître

  1. <string:ref> au lieu de <path:ref> → les slashes de HLP/2026/00006 cassent le routing, 404 garanti.
  2. Oublier website=True sur une route type='http' qui utilise website.layout → le layout ne charge pas le thème, rendu cassé.
  3. auth='public' + accès direct au modèle sans .sudo()AccessError : l'utilisateur public n'a pas le droit de lire helpdesk.ticket. Utiliser .sudo() ET filtrer les champs exposés côté template.
  4. csrf=False sur une route POST publique type='http' sans compensation → porte d'entrée pour scripts malveillants. Ajouter signature, IP whitelist ou rate limiting.
  5. Retourner un objet ORM sans .ids depuis un endpoint JSON → erreur de sérialisation Object of type RecordSet is not JSON serializable. Toujours transformer en list[dict] avant de renvoyer.
  6. Oublier auth='bearer' pour les APIs externes — réinventer un système de tokens custom est douloureux et fragile. Utiliser le natif v17+.
  7. Route type='json' appelée via GET → 405 Method Not Allowed. Les endpoints JSON-RPC Odoo attendent POST.

🎯 Tu as complété le Parcours Fondamentaux

23 articles, 5 blocs, un module helpdesk construit bloc par bloc de T06 à T23. Tu maîtrises maintenant les fondamentaux pour développer et maintenir un module Odoo 19 production-ready.

Bloc 1 — Installation
Ubuntu, Windows, Docker — 3 articles

Bloc 2 — Environnement dev
PyCharm, base de données, découverte — 4 articles

Bloc 3 — Framework ORM
Modèles, champs, relations, héritage, méthodes — 8 articles

Bloc 4 — Interface utilisateur
Form/List/Search, Kanban/Graph/Pivot, héritage, wizards — 4 articles

Bloc 5 — Rapports & automatisation
QWeb PDF, emails, cron/actions, controllers & API — 4 articles

La suite — d'autres parcours arrivent

Le Parcours Fondamentaux est un socle. À partir de cette base, d'autres parcours et articles spécialisés se publieront progressivement sur le blog.

📗 Parcours Avancé à venir

Pour aller plus loin dans le framework.

  • Composants OWL customs
  • Tests unitaires & intégration
  • Migration v18 → v19
  • Debug & profiling performance

📙 Parcours DevOps Odoo à venir

Industrialiser la prod.

  • CI/CD (GitHub Actions, GitLab)
  • Monitoring (Grafana, Loki)
  • Scaling workers & reverse proxy
  • Sauvegardes automatisées

🏷 Articles transverses par tag

En complément des parcours linéaires, des articles isolés regroupés par couche technique — chacun autonome, pas besoin de lire les précédents :

#framework-orm #web-ui #integrations #infrastructure

Voir tous les articles du blog Développement Odoo →

Voir aussi dans cette série

T20 — Rapports QWeb PDF

ir.actions.report

T21 — Email templates

mail.template

T22 — Actions serveur & cron

ir.cron, base.automation

Guide technique Odoo 19 — version PDF

Le Parcours Fondamentaux synthétisé en un guide de référence téléchargeable, avec le module helpdesk complet prêt à installer. Mis à jour à chaque nouvel article publié. Gratuit.

Télécharger le guide (PDF gratuit)
Actions serveur, cron et automations en Odoo 19 : ir.cron, ir.actions.server et base.automation
Bloc 5 · Rapports et automatisations — Article 3/4 Actions serveur, cron et automations en Odoo 19 Trois outils natifs, zéro serveur externe : automatiser les…