Get rid of github.com/go-chi/render use (#1919)

Replace go-chi/render with go-pkgz/rest for JSON responses and custom
helpers for HTML/plain text responses.

Key changes:
- Replace render.JSON/render.Status with rest.RenderJSON and explicit
  w.WriteHeader() calls
- Replace render.DecodeJSON with json.NewDecoder().Decode()
- Add SendErrorJSON helper that sets Content-Type header before
  WriteHeader (required since rest.RenderJSON can't set headers after
  WriteHeader is called)
- Add HTMLResponse and PlainTextResponse helpers

Fix export double-execution in migrator.go:
The original code called Export twice - once to io.Discard to check for
errors, then again to actually write. This was wasteful and had a race
condition risk. Now file mode buffers to memory first for atomic
success/failure, while stream mode writes directly with proper error
handling.
This commit is contained in:
Dmitry Verkhoturov
2025-12-03 11:41:29 -06:00
committed by GitHub
parent 34ea4c3e83
commit baf0db1947
8 changed files with 91 additions and 78 deletions
+10 -16
View File
@@ -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})
}
+30 -21
View File
@@ -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
+1 -3
View File
@@ -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
+21 -23
View File
@@ -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 {
+2 -3
View File
@@ -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()))
+6 -6
View File
@@ -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
+20 -5
View File
@@ -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 {
+1 -1
View File
@@ -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