# HG changeset patch
# User Christophe de Vienne <christophe@cdevienne.info>
# Date 1743088564 -3600
#      Thu Mar 27 16:16:04 2025 +0100
# Node ID 27128e5c0b240884f85a28ea13ce2d00e44fd5a4
# Parent  2c15f4f87eb5e06b5dbfa8a3b94e70192938e3a8
api: return friendly errors on template loading

diff --git a/engines/mustache/engine.go b/engines/mustache/engine.go
--- a/engines/mustache/engine.go
+++ b/engines/mustache/engine.go
@@ -2,11 +2,13 @@
 
 import (
 	"context"
+	"fmt"
 	"reflect"
 
 	"github.com/orus-io/mustache"
 	"github.com/rs/zerolog"
 
+	redner "orus.io/orus-io/rednerd/lib"
 	"orus.io/orus-io/rednerd/models"
 	"orus.io/orus-io/rednerd/rendering"
 )
@@ -32,7 +34,7 @@
 
 	bodyTemplate, err := mustache.ParseString(s)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("%w: %w", redner.ErrInvalidInput, err)
 	}
 
 	t := Template{
@@ -45,7 +47,7 @@
 	for name, meta := range template.Metadata {
 		tmpl, err := mustache.ParseString(meta)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("%w: metadata %s: %w", redner.ErrInvalidInput, name, err)
 		}
 
 		t.metadataTemplates[name] = tmpl
diff --git a/lib/render.go b/lib/render.go
--- a/lib/render.go
+++ b/lib/render.go
@@ -187,6 +187,10 @@
 			ctx, r.registry, template, metadata, toType,
 		)
 		if err != nil {
+			if errors.Is(err, ErrInvalidInput) {
+				return nil, models.NewRenderFailedError(err, nil)
+			}
+
 			if errors.Is(err, rendering.ErrUnknownTemplateLanguage) {
 				return nil, fmt.Errorf("%w: unknown template language: %s", ErrInvalidInput, template.Language)
 			}
diff --git a/tests/render_test.go b/tests/render_test.go
--- a/tests/render_test.go
+++ b/tests/render_test.go
@@ -22,6 +22,31 @@
 	tester.CreateUser("janedoe", "janedoe")
 	tester.Login("janedoe", "janedoe")
 
+	t.Run("Mustache-parse-errors", func(t *testing.T) {
+		defer tester.SetT(t)()
+
+		var result models.RenderFailedError
+
+		tester.RenderExpectingError(
+			&models.RenderRequest{
+				Accept: "text/html",
+				Template: &models.Template{
+					Language:   "mustache",
+					Produces:   "text/plain",
+					BodyFormat: "text",
+					Body:       "{{#toto}} {{/titi}}",
+				},
+				Data: models.Dataset{
+					apitester.JSONObj{"name": "Nath"},
+				},
+			},
+			&result,
+		)
+
+		assert.Equal(t, "invalid input: line 1: interleaved closing tag: titi", result.Message)
+		assert.Len(t, result.Details, 0)
+	})
+
 	t.Run("MJML-Template-to-html", func(t *testing.T) {
 		defer tester.SetT(t)()
 
diff --git a/testutils/apitester/apitester_template_management.go b/testutils/apitester/apitester_template_management.go
--- a/testutils/apitester/apitester_template_management.go
+++ b/testutils/apitester/apitester_template_management.go
@@ -5,6 +5,7 @@
 	"net/http"
 
 	"github.com/k3a/html2text"
+	"github.com/steinfletcher/apitest"
 	"github.com/stretchr/testify/require"
 
 	"orus.io/orus-io/rednerd/models"
@@ -48,23 +49,39 @@
 	r.JSON(response)
 }
 
+func (tester *APITester) renderRequest(
+	data *models.RenderRequest,
+) *apitest.Response {
+	apitest := tester.APITest("render")
+	if tester.debug {
+		apitest = apitest.Debug()
+	}
+
+	return apitest.
+		Post("/api/v1/render/").
+		JSON(data).
+		Expect(tester.t)
+}
+
 // Render renders some data and returns a document.
 func (tester *APITester) Render(
 	data *models.RenderRequest,
 	result *[]*models.Document,
 ) {
-	apitest := tester.APITest("render")
-	if tester.debug {
-		apitest = apitest.Debug()
-	}
+	tester.renderRequest(data).
+		Status(http.StatusOK).
+		End().
+		JSON(result)
+}
 
-	r := apitest.
-		Post("/api/v1/render/").
-		JSON(data).
-		Expect(tester.t).
-		Status(http.StatusOK).
-		End()
-	r.JSON(result)
+func (tester *APITester) RenderExpectingError(
+	data *models.RenderRequest,
+	err *models.RenderFailedError,
+) {
+	tester.renderRequest(data).
+		Status(http.StatusBadRequest).
+		End().
+		JSON(err)
 }
 
 // Renderlogs gives us a csv document containing the