diff --git a/internal/apierror/api_error.go b/internal/apierror/api_error.go
new file mode 100644
index 0000000..003e016
--- /dev/null
+++ b/internal/apierror/api_error.go
@@ -0,0 +1,17 @@
+package apierror
+
+import "fmt"
+
+type APIError struct {
+ StatusCode int `json:"-"`
+ Code string `json:"code"`
+ Message string `json:"error"`
+}
+
+func New(status int, code, message string) *APIError {
+ return &APIError{status, code, message}
+}
+
+func (err *APIError) Error() string {
+ return fmt.Sprintf("%s: %s", err.Code, err.Message)
+}
diff --git a/internal/template/errors.go b/internal/template/errors.go
new file mode 100644
index 0000000..a61c397
--- /dev/null
+++ b/internal/template/errors.go
@@ -0,0 +1,9 @@
+package template
+
+type errBadTemplate struct {
+ message string
+}
+
+func (err *errBadTemplate) Error() string {
+ return err.message
+}
diff --git a/internal/template/http_handlers.go b/internal/template/http_handlers.go
index c08007c..d0c5c4e 100644
--- a/internal/template/http_handlers.go
+++ b/internal/template/http_handlers.go
@@ -7,6 +7,7 @@ import (
"github.com/labstack/echo/v4"
"io"
"net/http"
+ "tesseract/internal/apierror"
"tesseract/internal/service"
)
@@ -136,6 +137,10 @@ func buildTemplate(c echo.Context, body postTemplateRequestBody) error {
buildArgs: body.BuildArgs,
})
if err != nil {
+ var errBadTemplate *errBadTemplate
+ if errors.As(err, &errBadTemplate) {
+ return apierror.New(http.StatusBadRequest, "BAD_TEMPLATE", errBadTemplate.message)
+ }
return err
}
diff --git a/internal/template/template_manager.go b/internal/template/template_manager.go
index 7421418..b7181ee 100644
--- a/internal/template/template_manager.go
+++ b/internal/template/template_manager.go
@@ -11,8 +11,10 @@ import (
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
+ "github.com/docker/docker/errdefs"
"github.com/google/uuid"
"github.com/uptrace/bun"
+ "strings"
"time"
)
@@ -252,6 +254,14 @@ func (mgr *templateManager) buildTemplate(ctx context.Context, template *templat
BuildArgs: opts.buildArgs,
})
if err != nil {
+ if errdefs.IsInvalidParameter(err) {
+ return nil, &errBadTemplate{
+ // the docker sdk returns an error message that looks like:
+ // "Error response from daemon: dockerfile parse error on line 1: unknown instruction: FR (did you mean FROM?)"
+ // we don't want the "error response..." part because it is meaningless
+ message: strings.Replace(err.Error(), "Error response from daemon: ", "", 1),
+ }
+ }
return nil, err
}
diff --git a/main.go b/main.go
index 9d82856..87e6a7e 100644
--- a/main.go
+++ b/main.go
@@ -10,6 +10,7 @@ import (
"net/http"
"os"
"path/filepath"
+ "tesseract/internal/apierror"
"tesseract/internal/migration"
"tesseract/internal/service"
"tesseract/internal/template"
@@ -86,10 +87,20 @@ func main() {
c.Logger().Error(err)
_ = c.NoContent(http.StatusInternalServerError)
}
- } else {
- c.Logger().Error(err)
- _ = c.NoContent(http.StatusInternalServerError)
+ return
}
+
+ var apiErr *apierror.APIError
+ if errors.As(err, &apiErr) {
+ if err = c.JSON(apiErr.StatusCode, apiErr); err != nil {
+ c.Logger().Error(err)
+ _ = c.NoContent(http.StatusInternalServerError)
+ }
+ return
+ }
+
+ c.Logger().Error(err)
+ _ = c.NoContent(http.StatusInternalServerError)
}
apiServer.Logger.Fatal(apiServer.Start(":8080"))
diff --git a/web/src/api.ts b/web/src/api.ts
index 1f23303..1ff2fe0 100644
--- a/web/src/api.ts
+++ b/web/src/api.ts
@@ -1,5 +1,12 @@
import { promiseOrThrow } from "./lib/errors";
+interface ApiErrorResponse {
+ code: string;
+ error: string;
+}
+
+const API_ERROR_BAD_TEMPLATE = "BAD_TEMPLATE";
+
enum ApiError {
NotFound = "NOT_FOUND",
BadRequest = "BAD_REQUEST",
@@ -16,10 +23,9 @@ async function fetchApi(
() => ApiError.Network,
);
if (res.status !== 200) {
- console.log(res.status);
switch (res.status) {
- case 401:
- throw ApiError.BadRequest;
+ case 400:
+ throw await res.json();
case 404:
throw ApiError.NotFound;
default:
@@ -29,4 +35,15 @@ async function fetchApi(
return res;
}
-export { ApiError, fetchApi };
+function isApiErrorResponse(error: unknown): error is ApiErrorResponse {
+ return (
+ error !== null &&
+ error !== undefined &&
+ typeof error === "object" &&
+ "code" in error &&
+ "error" in error
+ );
+}
+
+export { API_ERROR_BAD_TEMPLATE, ApiError, fetchApi, isApiErrorResponse };
+export type { ApiErrorResponse };
diff --git a/web/src/templates/api.ts b/web/src/templates/api.ts
index 751f2c7..1ffed05 100644
--- a/web/src/templates/api.ts
+++ b/web/src/templates/api.ts
@@ -144,6 +144,10 @@ async function buildTemplate({
Accept: "text/event-stream",
},
});
+ if (res.status !== 200) {
+ const errBody = await res.json();
+ throw errBody;
+ }
const stream = res.body?.pipeThrough(new TextDecoderStream()).getReader();
if (stream) {
while (true) {
diff --git a/web/src/templates/template-editor-store.tsx b/web/src/templates/template-editor-store.tsx
index 28b5feb..bcc96fa 100644
--- a/web/src/templates/template-editor-store.tsx
+++ b/web/src/templates/template-editor-store.tsx
@@ -2,6 +2,7 @@ import { createStore, useStore } from "zustand";
import type { Template } from "./types";
import { createContext, useContext } from "react";
import { buildTemplate } from "./api";
+import { isApiErrorResponse, type ApiErrorResponse } from "@/api";
interface TemplateEditorState {
template: Template;
@@ -9,6 +10,7 @@ interface TemplateEditorState {
isBuildInProgress: boolean;
isBuildOutputVisible: boolean;
buildOutput: string;
+ buildError: ApiErrorResponse | null;
startBuild: ({
imageTag,
@@ -34,6 +36,7 @@ function createTemplateEditorStore({
isBuildInProgress: false,
isBuildOutputVisible: false,
buildOutput: "",
+ buildError: null,
startBuild: async ({ imageTag, buildArgs }) => {
const state = get();
@@ -42,6 +45,7 @@ function createTemplateEditorStore({
isBuildInProgress: true,
isBuildOutputVisible: true,
buildOutput: "",
+ buildError: null,
});
try {
@@ -51,8 +55,12 @@ function createTemplateEditorStore({
templateName: state.template.name,
onBuildOutput: state.addBuildOutputChunk,
});
- } catch {
- // TODO: handle build error
+ } catch (error) {
+ console.error(error);
+ if (isApiErrorResponse(error)) {
+ console.log("askdjskdjk");
+ set({ buildError: error });
+ }
} finally {
set({ isBuildInProgress: false });
}
diff --git a/web/src/templates/template-editor.tsx b/web/src/templates/template-editor.tsx
index 0b430c9..b866f69 100644
--- a/web/src/templates/template-editor.tsx
+++ b/web/src/templates/template-editor.tsx
@@ -1,4 +1,4 @@
-import { ApiError } from "@/api";
+import { API_ERROR_BAD_TEMPLATE, ApiError } from "@/api";
import { CodeMirrorEditor } from "@/components/codemirror-editor";
import { Button } from "@/components/ui/button.tsx";
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
@@ -35,7 +35,7 @@ import {
} from "./template-editor-store";
import type { Template } from "./types";
import { useToast } from "@/hooks/use-toast";
-import { ToastAction } from "@/components/ui/toast";
+import { Toaster } from "@/components/ui/toaster";
function TemplateEditor() {
const { templateName, _splat } = templateEditorRoute.useParams();
@@ -122,6 +122,8 @@ function _TemplateEditor({
+
+
);
@@ -332,4 +334,33 @@ function BuildOutput() {
);
}
+function BuildErrorToast() {
+ const buildError = useTemplateEditorStore((state) => state.buildError);
+ const { toast } = useToast();
+
+ useEffect(() => {
+ if (!buildError) return;
+
+ switch (buildError.code) {
+ case API_ERROR_BAD_TEMPLATE:
+ toast({
+ variant: "destructive",
+ title: "Invalid template",
+ description: buildError.error,
+ });
+ break;
+
+ default:
+ toast({
+ variant: "destructive",
+ title: "Unexpected error",
+ description: buildError.error,
+ });
+ break;
+ }
+ }, [buildError, toast]);
+
+ return false;
+}
+
export { TemplateEditor };