diff --git a/backend/app/rest/api/admin.go b/backend/app/rest/api/admin.go index 9b6809a54f..2902566777 100644 --- a/backend/app/rest/api/admin.go +++ b/backend/app/rest/api/admin.go @@ -7,7 +7,6 @@ import ( "time" "github.com/go-chi/chi/v5" - "github.com/go-chi/render" "github.com/go-pkgz/auth/v2" cache "github.com/go-pkgz/lcw/v2" log "github.com/go-pkgz/lgr" @@ -54,8 +53,7 @@ func (a *admin) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) { return } a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.SiteID, locator.URL, lastCommentsScope)) - render.Status(r, http.StatusOK) - render.JSON(w, r, R.JSON{"id": id, "locator": locator}) + R.RenderJSON(w, R.JSON{"id": id, "locator": locator}) } // DELETE /user/{userid}?site=side-id - delete all user comments for requested userid @@ -69,8 +67,7 @@ func (a *admin) deleteUserCtrl(w http.ResponseWriter, r *http.Request) { return } a.cache.Flush(cache.Flusher(siteID).Scopes(userID, siteID, lastCommentsScope)) - render.Status(r, http.StatusOK) - render.JSON(w, r, R.JSON{"user_id": userID, "site_id": siteID}) + R.RenderJSON(w, R.JSON{"user_id": userID, "site_id": siteID}) } // GET /user/{userid}?site=side-id - get user info for requested userid @@ -84,8 +81,7 @@ func (a *admin) getUserInfoCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get user info", rest.ErrInternal) return } - render.Status(r, http.StatusOK) - render.JSON(w, r, ucomments[0].User) + R.RenderJSON(w, ucomments[0].User) } // GET /deleteme?token=jwt - delete all user comments and details by user's request. Gets info about deleted used from provided token @@ -135,8 +131,7 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) { } a.cache.Flush(cache.Flusher(audience).Scopes(audience, claims.User.ID, lastCommentsScope)) - render.Status(r, http.StatusOK) - render.JSON(w, r, R.JSON{"user_id": claims.User.ID, "site_id": claims.Audience}) + R.RenderJSON(w, R.JSON{"user_id": claims.User.ID, "site_id": claims.Audience}) } // PUT /user/{userid}?site=side-id&block=1&ttl=7d - block or unblock user @@ -164,7 +159,7 @@ func (a *admin) setBlockCtrl(w http.ResponseWriter, r *http.Request) { } } a.cache.Flush(cache.Flusher(siteID).Scopes(userID, siteID, lastCommentsScope)) - render.JSON(w, r, R.JSON{"user_id": userID, "site_id": siteID, "block": blockStatus}) + R.RenderJSON(w, R.JSON{"user_id": userID, "site_id": siteID, "block": blockStatus}) } // GET /blocked?site=siteID - list blocked users @@ -175,7 +170,7 @@ func (a *admin) blockedUsersCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get blocked users", rest.ErrSiteNotFound) return } - render.JSON(w, r, users) + R.RenderJSON(w, users) } // PUT /readonly?site=siteID&url=post-url&ro=1 - set or reset read-only status for the post @@ -202,7 +197,7 @@ func (a *admin) setReadOnlyCtrl(w http.ResponseWriter, r *http.Request) { return } a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL, locator.SiteID)) - render.JSON(w, r, R.JSON{"locator": locator, "read-only": roStatus}) + R.RenderJSON(w, R.JSON{"locator": locator, "read-only": roStatus}) } // PUT /title/{id}?site=siteID&url=post-url - set comment PostTitle to page's title @@ -218,8 +213,7 @@ func (a *admin) setTitleCtrl(w http.ResponseWriter, r *http.Request) { log.Printf("[INFO] set comment's title %s to %q", id, c.PostTitle) a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL, lastCommentsScope)) - render.Status(r, http.StatusOK) - render.JSON(w, r, R.JSON{"id": id, "locator": locator}) + R.RenderJSON(w, R.JSON{"id": id, "locator": locator}) } // PUT /verify?site=siteID&url=post-url&ro=1 - set or reset read-only status for the post @@ -233,7 +227,7 @@ func (a *admin) setVerifyCtrl(w http.ResponseWriter, r *http.Request) { return } a.cache.Flush(cache.Flusher(siteID).Scopes(siteID, userID)) - render.JSON(w, r, R.JSON{"user": userID, "verified": verifyStatus}) + R.RenderJSON(w, R.JSON{"user": userID, "verified": verifyStatus}) } // PUT /pin/{id}?site=siteID&url=post-url&pin=1 @@ -248,5 +242,5 @@ func (a *admin) setPinCtrl(w http.ResponseWriter, r *http.Request) { return } a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL)) - render.JSON(w, r, R.JSON{"id": commentID, "locator": locator, "pin": pinStatus}) + R.RenderJSON(w, R.JSON{"id": commentID, "locator": locator, "pin": pinStatus}) } diff --git a/backend/app/rest/api/migrator.go b/backend/app/rest/api/migrator.go index d2b52f5703..d519acaed3 100644 --- a/backend/app/rest/api/migrator.go +++ b/backend/app/rest/api/migrator.go @@ -1,16 +1,17 @@ package api import ( + "bytes" "compress/gzip" "context" "fmt" "io" "net/http" "os" + "strconv" "sync" "time" - "github.com/go-chi/render" cache "github.com/go-pkgz/lcw/v2" log "github.com/go-pkgz/lgr" R "github.com/go-pkgz/rest" @@ -58,8 +59,8 @@ func (m *Migrator) importCtrl(w http.ResponseWriter, r *http.Request) { go m.runImport(siteID, r.URL.Query().Get("provider"), tmpfile) // import runs in background and sets busy flag for site - render.Status(r, http.StatusAccepted) - render.JSON(w, r, R.JSON{"status": "import request accepted"}) + w.WriteHeader(http.StatusAccepted) + R.RenderJSON(w, R.JSON{"status": "import request accepted"}) } // POST /import/form?secret=key&site=site-id&provider=disqus|remark|wordpress @@ -93,8 +94,8 @@ func (m *Migrator) importFormCtrl(w http.ResponseWriter, r *http.Request) { go m.runImport(siteID, r.URL.Query().Get("provider"), tmpfile) // import runs in background and sets busy flag for site - render.Status(r, http.StatusAccepted) - render.JSON(w, r, R.JSON{"status": "import request accepted"}) + w.WriteHeader(http.StatusAccepted) + R.RenderJSON(w, R.JSON{"status": "import request accepted"}) } // GET /wait?site=site-id @@ -114,14 +115,13 @@ func (m *Migrator) waitCtrl(w http.ResponseWriter, r *http.Request) { select { case <-ctx.Done(): - render.Status(r, http.StatusGatewayTimeout) - render.JSON(w, r, R.JSON{"status": "timeout expired", "site_id": siteID}) + w.WriteHeader(http.StatusGatewayTimeout) + R.RenderJSON(w, R.JSON{"status": "timeout expired", "site_id": siteID}) return case <-time.After(100 * time.Millisecond): } } - render.Status(r, http.StatusOK) - render.JSON(w, r, R.JSON{"status": "completed", "site_id": siteID}) + R.RenderJSON(w, R.JSON{"status": "completed", "site_id": siteID}) } // GET /export?site=site-id&secret=12345&?mode=file|stream @@ -129,23 +129,32 @@ func (m *Migrator) waitCtrl(w http.ResponseWriter, r *http.Request) { func (m *Migrator) exportCtrl(w http.ResponseWriter, r *http.Request) { siteID := r.URL.Query().Get("site") - var writer io.Writer = w if r.URL.Query().Get("mode") == "file" { + // buffer to memory to handle errors before committing to response + var buf bytes.Buffer + gzWriter := gzip.NewWriter(&buf) + if _, err := m.NativeExporter.Export(gzWriter, siteID); err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed", rest.ErrInternal) + return + } + if err := gzWriter.Close(); err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed", rest.ErrInternal) + return + } + exportFile := fmt.Sprintf("%s-%s.json.gz", siteID, time.Now().Format("20060102")) w.Header().Set("Content-Type", "application/gzip") w.Header().Set("Content-Disposition", "attachment;filename="+exportFile) - gzWriter := gzip.NewWriter(w) - defer func() { - if e := gzWriter.Close(); e != nil { - log.Printf("[WARN] can't close gzip writer, %s", e) - } - }() - writer = gzWriter + w.Header().Set("Content-Length", strconv.Itoa(buf.Len())) + if _, err := io.Copy(w, &buf); err != nil { + log.Printf("[WARN] failed to write export response: %v", err) + } + return } - if _, err := m.NativeExporter.Export(writer, siteID); err != nil { + // stream mode - write directly to response + if _, err := m.NativeExporter.Export(w, siteID); err != nil { rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed", rest.ErrInternal) - return } } @@ -201,8 +210,8 @@ func (m *Migrator) remapCtrl(w http.ResponseWriter, r *http.Request) { log.Printf("[DEBUG] convert request completed. site=%s, comments=%d", siteID, size) }() - render.Status(r, http.StatusAccepted) - render.JSON(w, r, R.JSON{"status": "convert request accepted"}) + w.WriteHeader(http.StatusAccepted) + R.RenderJSON(w, R.JSON{"status": "convert request accepted"}) } // runImport reads from tmpfile and import for given siteID and provider diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index 5922fc3045..0abdd98a50 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -20,7 +20,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" - "github.com/go-chi/render" "github.com/go-pkgz/auth/v2" "github.com/go-pkgz/lcw/v2" log "github.com/go-pkgz/lgr" @@ -473,8 +472,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) { if cnf.Admins == nil { // prevent json serialization to nil cnf.Admins = []string{} } - render.Status(r, http.StatusOK) - render.JSON(w, r, cnf) + R.RenderJSON(w, cnf) } // serves static files from the webRoot directory or files embedded into the compiled binary if that directory is absent diff --git a/backend/app/rest/api/rest_private.go b/backend/app/rest/api/rest_private.go index cbe957929e..35788b42b1 100644 --- a/backend/app/rest/api/rest_private.go +++ b/backend/app/rest/api/rest_private.go @@ -15,7 +15,6 @@ import ( "time" "github.com/go-chi/chi/v5" - "github.com/go-chi/render" "github.com/go-pkgz/auth/v2" "github.com/go-pkgz/auth/v2/token" cache "github.com/go-pkgz/lcw/v2" @@ -77,7 +76,7 @@ func (s *private) previewCommentCtrl(w http.ResponseWriter, r *http.Request) { user := rest.MustGetUserInfo(r) comment := store.Comment{} - if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &comment); err != nil { + if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, hardBodyLimit)).Decode(&comment); err != nil { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment", rest.ErrDecode) return } @@ -100,14 +99,13 @@ func (s *private) previewCommentCtrl(w http.ResponseWriter, r *http.Request) { return } } - - render.HTML(w, r, comment.Text) + rest.HTMLResponse(w, http.StatusOK, comment.Text) } // POST /comment - adds comment, resets all immutable fields func (s *private) createCommentCtrl(w http.ResponseWriter, r *http.Request) { comment := store.Comment{} - if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &comment); err != nil { + if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, hardBodyLimit)).Decode(&comment); err != nil { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment", rest.ErrDecode) return } @@ -176,8 +174,8 @@ func (s *private) createCommentCtrl(w http.ResponseWriter, r *http.Request) { log.Printf("[DEBUG] created comment %+v", finalComment) - render.Status(r, http.StatusCreated) - render.JSON(w, r, &finalComment) + w.WriteHeader(http.StatusCreated) + R.RenderJSON(w, &finalComment) } // PUT /comment/{id}?site=siteID&url=post-url - update comment @@ -188,7 +186,7 @@ func (s *private) updateCommentCtrl(w http.ResponseWriter, r *http.Request) { Delete bool }{} - if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &edit); err != nil { + if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, hardBodyLimit)).Decode(&edit); err != nil { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't read comment details from body", rest.ErrDecode) return } @@ -233,7 +231,7 @@ func (s *private) updateCommentCtrl(w http.ResponseWriter, r *http.Request) { } s.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.SiteID, locator.URL, lastCommentsScope, user.ID)) - render.JSON(w, r, res) + R.RenderJSON(w, res) } // GET /user?site=siteID - returns user info @@ -251,7 +249,7 @@ func (s *private) userInfoCtrl(w http.ResponseWriter, r *http.Request) { } } - render.JSON(w, r, user) + R.RenderJSON(w, user) } // PUT /vote/{id}?site=siteID&url=post-url&vote=1 - vote for/against comment @@ -292,7 +290,7 @@ func (s *private) voteCtrl(w http.ResponseWriter, r *http.Request) { return } s.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL, comment.User.ID)) - render.JSON(w, r, R.JSON{"id": comment.ID, "score": comment.Score}) + R.RenderJSON(w, R.JSON{"id": comment.ID, "score": comment.Score}) } // getEmailCtrl gets email address for authenticated user. @@ -305,7 +303,7 @@ func (s *private) getEmailCtrl(w http.ResponseWriter, r *http.Request) { log.Printf("[WARN] can't read email for %s, %v", user.ID, err) } - render.JSON(w, r, R.JSON{"user": user, "address": address}) + R.RenderJSON(w, R.JSON{"user": user, "address": address}) } // sendEmailConfirmationCtrl gets address and siteID from query, makes confirmation token and sends it to user. @@ -322,7 +320,7 @@ func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Reque Address string autoConfirm bool }{autoConfirm: true} - if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &subscribe); err != nil { + if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, hardBodyLimit)).Decode(&subscribe); err != nil { if err != io.EOF { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't parse request body", rest.ErrDecode) return @@ -385,7 +383,7 @@ func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Reque }, ) - render.JSON(w, r, R.JSON{"user": user, "address": subscribe.Address, "updated": false}) + R.RenderJSON(w, R.JSON{"user": user, "address": subscribe.Address, "updated": false}) } // telegramSubscribeCtrl generates and verifies telegram notification request @@ -423,7 +421,7 @@ func (s *private) telegramSubscribeCtrl(w http.ResponseWriter, r *http.Request) s.telegramService.AddToken(tkn, user.ID, siteID, expires) - render.JSON(w, r, R.JSON{"token": tkn, "bot": s.telegramService.GetBotUsername()}) + R.RenderJSON(w, R.JSON{"token": tkn, "bot": s.telegramService.GetBotUsername()}) return } @@ -445,7 +443,7 @@ func (s *private) telegramSubscribeCtrl(w http.ResponseWriter, r *http.Request) return } - render.JSON(w, r, R.JSON{"updated": true, "address": val}) + R.RenderJSON(w, R.JSON{"updated": true, "address": val}) } // setConfirmedEmailCtrl uses provided token parameter (generated by sendEmailConfirmationCtrl) to set email and add it to user token @@ -457,7 +455,7 @@ func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request) Site string Token string }{} - if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &confirm); err != nil { + if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, hardBodyLimit)).Decode(&confirm); err != nil { if err != io.EOF { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't parse request body", rest.ErrDecode) return @@ -513,7 +511,7 @@ func (s *private) setEmail(w http.ResponseWriter, r *http.Request, userID, siteI rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token", rest.ErrInternal) return } - render.JSON(w, r, R.JSON{"updated": true, "address": val}) + R.RenderJSON(w, R.JSON{"updated": true, "address": val}) } // POST/GET /email/unsubscribe.html?site=siteID&tkn=jwt - unsubscribe the user in token from email notifications @@ -597,7 +595,7 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) { tmpl := template.Must(template.New("unsubscribe").Parse(tmplstr)) msg := bytes.Buffer{} MustExecute(tmpl, &msg, nil) - render.HTML(w, r, msg.String()) + rest.HTMLResponse(w, http.StatusOK, msg.String()) } // DELETE /email?site=siteID - removes user's email @@ -624,7 +622,7 @@ func (s *private) deleteEmailCtrl(w http.ResponseWriter, r *http.Request) { return } } - render.JSON(w, r, R.JSON{"deleted": true}) + R.RenderJSON(w, R.JSON{"deleted": true}) } // DELETE /telegram?site=siteID - removes user's telegram @@ -638,7 +636,7 @@ func (s *private) deleteTelegramCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete telegram for user", code) return } - render.JSON(w, r, R.JSON{"deleted": true}) + R.RenderJSON(w, R.JSON{"deleted": true}) } // GET /userdata?site=siteID - exports all data about the user as a json with user info and list of all comments @@ -726,7 +724,7 @@ func (s *private) deleteMeCtrl(w http.ResponseWriter, r *http.Request) { } link := fmt.Sprintf("%s/web/deleteme.html?token=%s", s.remarkURL, tokenStr) - render.JSON(w, r, R.JSON{"site": siteID, "user_id": user.ID, "token": tokenStr, "link": link}) + R.RenderJSON(w, R.JSON{"site": siteID, "user_id": user.ID, "token": tokenStr, "link": link}) } // POST /image - save image with form request @@ -751,7 +749,7 @@ func (s *private) savePictureCtrl(w http.ResponseWriter, r *http.Request) { return } - render.JSON(w, r, R.JSON{"id": id}) + R.RenderJSON(w, R.JSON{"id": id}) } func (s *private) isReadOnly(locator store.Locator) bool { diff --git a/backend/app/rest/api/rest_private_test.go b/backend/app/rest/api/rest_private_test.go index 8bdb440d16..07b076b132 100644 --- a/backend/app/rest/api/rest_private_test.go +++ b/backend/app/rest/api/rest_private_test.go @@ -16,7 +16,6 @@ import ( "testing" "time" - "github.com/go-chi/render" "github.com/go-pkgz/auth/v2/token" "github.com/go-pkgz/lgr" R "github.com/go-pkgz/rest" @@ -970,7 +969,7 @@ func TestRest_EmailNotification(t *testing.T) { require.NoError(t, resp.Body.Close()) require.Equal(t, http.StatusCreated, resp.StatusCode, string(body)) parentComment := store.Comment{} - require.NoError(t, render.DecodeJSON(strings.NewReader(string(body)), &parentComment)) + require.NoError(t, json.Unmarshal(body, &parentComment)) // wait for mock notification Submit to kick off time.Sleep(time.Millisecond * 30) require.Equal(t, 1, len(mockDestination.Get())) @@ -1220,7 +1219,7 @@ func TestRest_TelegramNotification(t *testing.T) { require.NoError(t, resp.Body.Close()) require.Equal(t, http.StatusCreated, resp.StatusCode, string(body)) parentComment := store.Comment{} - require.NoError(t, render.DecodeJSON(strings.NewReader(string(body)), &parentComment)) + require.NoError(t, json.Unmarshal(body, &parentComment)) // wait for mock notification Submit to kick off time.Sleep(time.Millisecond * 30) require.Equal(t, 1, len(mockDestination.Get())) diff --git a/backend/app/rest/api/rest_public.go b/backend/app/rest/api/rest_public.go index f766c33cfb..48981e3baf 100644 --- a/backend/app/rest/api/rest_public.go +++ b/backend/app/rest/api/rest_public.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/sha1" // nolint "encoding/base64" + "encoding/json" "fmt" "io" "net/http" @@ -12,7 +13,6 @@ import ( "time" "github.com/go-chi/chi/v5" - "github.com/go-chi/render" cache "github.com/go-pkgz/lcw/v2" log "github.com/go-pkgz/lgr" R "github.com/go-pkgz/rest" @@ -232,7 +232,6 @@ func (s *public) commentByIDCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by id", rest.ErrCommentNotFound) return } - render.Status(r, http.StatusOK) if err = R.RenderJSONWithHTML(w, r, comment); err != nil { log.Printf("[WARN] can't render last comments for url=%s, id=%s", url, id) @@ -297,7 +296,7 @@ func (s *public) countCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get count", rest.ErrPostNotFound) return } - render.JSON(w, r, R.JSON{"count": count, "locator": locator}) + R.RenderJSON(w, R.JSON{"count": count, "locator": locator}) } // POST /counts?site=siteID - get number of comments for posts from post body @@ -305,7 +304,7 @@ func (s *public) countMultiCtrl(w http.ResponseWriter, r *http.Request) { const countBodyLimit int64 = 1024 * 128 // count request can be big for some site because it lists all urls siteID := r.URL.Query().Get("site") posts := []string{} - if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, countBodyLimit), &posts); err != nil { + if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, countBodyLimit)).Decode(&posts); err != nil { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get list of posts from request", rest.ErrSiteNotFound) return } @@ -393,13 +392,14 @@ func (s *public) loadPictureCtrl(w http.ResponseWriter, r *http.Request) { } // GET /robots.txt -func (s *public) robotsCtrl(w http.ResponseWriter, r *http.Request) { +func (s *public) robotsCtrl(w http.ResponseWriter, _ *http.Request) { allowed := []string{"/find", "/last", "/id", "/count", "/counts", "/list", "/config", "/user", "/img", "/avatar", "/picture"} for i := range allowed { allowed[i] = "Allow: /api/v1" + allowed[i] } - render.PlainText(w, r, "User-agent: *\nDisallow: /auth/\nDisallow: /api/\n"+strings.Join(allowed, "\n")+"\n") + responseText := fmt.Sprintf("User-agent: *\nDisallow: /auth/\nDisallow: /api/\n%s\n", strings.Join(allowed, "\n")) + rest.PlainTextResponse(w, http.StatusOK, responseText) } // GET /qr/telegram - generates QR for provided URL, used for Telegram auth and notifications subscription. The first diff --git a/backend/app/rest/httperrors.go b/backend/app/rest/httperrors.go index 93c56e1c26..88ed3369fe 100644 --- a/backend/app/rest/httperrors.go +++ b/backend/app/rest/httperrors.go @@ -10,7 +10,6 @@ import ( "runtime" "strings" - "github.com/go-chi/render" log "github.com/go-pkgz/lgr" "github.com/go-pkgz/rest" @@ -67,20 +66,36 @@ func SendErrorHTML(w http.ResponseWriter, r *http.Request, httpStatusCode int, e tmplstr := MustRead("error_response.html.tmpl") tmpl := template.Must(template.New("error").Parse(tmplstr)) log.Printf("[WARN] %s", errDetailsMsg(r, httpStatusCode, err, details, errCode)) - render.Status(r, httpStatusCode) + msg := bytes.Buffer{} MustExecute(tmpl, &msg, errTmplData{ Error: err.Error(), Details: details, }) - render.HTML(w, r, msg.String()) + + HTMLResponse(w, httpStatusCode, msg.String()) } // SendErrorJSON makes {error: blah, details: blah, code: 42} json body and responds with error code func SendErrorJSON(w http.ResponseWriter, r *http.Request, httpStatusCode int, err error, details string, errCode int) { log.Printf("[WARN] %s", errDetailsMsg(r, httpStatusCode, err, details, errCode)) - render.Status(r, httpStatusCode) - render.JSON(w, r, rest.JSON{"error": err.Error(), "details": details, "code": errCode}) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(httpStatusCode) + rest.RenderJSON(w, rest.JSON{"error": err.Error(), "details": details, "code": errCode}) +} + +// HTMLResponse writes HTML content with the given status code +func HTMLResponse(w http.ResponseWriter, status int, html string) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(status) + _, _ = w.Write([]byte(html)) +} + +// PlainTextResponse writes plain text content with the given status code +func PlainTextResponse(w http.ResponseWriter, status int, text string) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(status) + _, _ = w.Write([]byte(text)) } func errDetailsMsg(r *http.Request, httpStatusCode int, err error, details string, errCode int) string { diff --git a/backend/go.mod b/backend/go.mod index 1f9303a1e0..b954a822e6 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -10,7 +10,6 @@ require ( github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/cors v1.2.1 - github.com/go-chi/render v1.0.3 github.com/go-pkgz/auth/v2 v2.0.1-0.20250415030422-4f9f2c5e3b0d github.com/go-pkgz/jrpc v0.3.1 github.com/go-pkgz/lcw/v2 v2.0.0 @@ -48,6 +47,7 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/didip/tollbooth/v8 v8.0.1 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/go-chi/render v1.0.3 // indirect github.com/go-oauth2/oauth2/v4 v4.5.3 // indirect github.com/go-pkgz/email v0.5.0 // indirect github.com/go-pkgz/expirable-cache/v3 v3.0.0 // indirect