this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(api) format api errors in json for consistency

eagleusb 6371c2dc f7cc7ade

+40 -15
+4 -1
internal/data/bitcoin.go
··· 23 23 client *http.Client 24 24 } 25 25 26 - // NewFetcher returns a Fetcher ready to load data. 26 + // NewBitcoinPrices return a bitcoin prices http loader. 27 27 func NewBitcoinPrices() *BitcoinPrices { 28 28 return &BitcoinPrices{ 29 29 client: &http.Client{ ··· 37 37 } 38 38 } 39 39 40 + // SetPrices set the prices from upstream and store them safely (mutex lock). 40 41 func (f *BitcoinPrices) SetPrices(ctx context.Context) error { 41 42 prices, err := f.fetchPrices(ctx) 42 43 if err != nil { ··· 51 52 return nil 52 53 } 53 54 55 + // GetPrices get the bitcoin prices from memory. 54 56 func (f *BitcoinPrices) GetPrices() []types.Price { 55 57 f.mu.RLock() 56 58 defer f.mu.RUnlock() 57 59 return f.prices 58 60 } 59 61 62 + // fetchPrices implement the upstream fetch of bitcoin prices. 60 63 func (f *BitcoinPrices) fetchPrices(ctx context.Context) ([]types.Price, error) { 61 64 req, err := http.NewRequestWithContext(ctx, http.MethodGet, datasourceURL, nil) 62 65 if err != nil {
+6 -6
internal/handlers/average.go
··· 10 10 "github.com/eagleusb/proxycon/internal/types" 11 11 ) 12 12 13 - // averageEndpoint is the handler function for `GET /average?start=<timestamp>&end=<timestamp>` 13 + // averageEndpoint is the handler function for `GET /average?start=<timestamp>&end=<timestamp>`. 14 14 func (h *HttpServer) averageEndpoint(w http.ResponseWriter, r *http.Request) { 15 15 startStr := r.URL.Query().Get("start") 16 16 endStr := r.URL.Query().Get("end") 17 17 18 18 if startStr == "" || endStr == "" { 19 - http.Error(w, "missing query parameters: start and end are required", http.StatusBadRequest) 19 + writeJSONError(w, "missing query parameters: start and end are required", http.StatusBadRequest) 20 20 return 21 21 } 22 22 23 23 startTime, err := time.Parse(time.RFC3339, startStr) 24 24 if err != nil { 25 - http.Error(w, "invalid start timestamp: must be RFC3339 format", http.StatusBadRequest) 25 + writeJSONError(w, "invalid start timestamp: must be RFC3339 format", http.StatusBadRequest) 26 26 return 27 27 } 28 28 29 29 endTime, err := time.Parse(time.RFC3339, endStr) 30 30 if err != nil { 31 - http.Error(w, "invalid end timestamp: must be RFC3339 format", http.StatusBadRequest) 31 + writeJSONError(w, "invalid end timestamp: must be RFC3339 format", http.StatusBadRequest) 32 32 return 33 33 } 34 34 35 35 if startTime.After(endTime) { 36 - http.Error(w, "start timestamp must be before end timestamp", http.StatusBadRequest) 36 + writeJSONError(w, "start timestamp must be before end timestamp", http.StatusBadRequest) 37 37 return 38 38 } 39 39 ··· 51 51 } 52 52 53 53 if len(matched) == 0 { 54 - http.Error(w, "no prices found in the given range", http.StatusNotFound) 54 + writeJSONError(w, "no prices found in the given range", http.StatusNotFound) 55 55 return 56 56 } 57 57
+15
internal/handlers/error.go
··· 1 + package handlers 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + 7 + "github.com/eagleusb/proxycon/internal/types" 8 + ) 9 + 10 + // writeJSONError writes a JSON-formatted error response for consistency. 11 + func writeJSONError(w http.ResponseWriter, message string, code int) { 12 + w.Header().Set("Content-Type", "application/json") 13 + w.WriteHeader(code) 14 + json.NewEncoder(w).Encode(types.ErrorResponse{Code: code, Message: message}) 15 + }
+3 -3
internal/handlers/list.go
··· 9 9 "github.com/eagleusb/proxycon/internal/types" 10 10 ) 11 11 12 - // listEndpoint is the handler function for `GET /list` 12 + // listEndpoint is the handler function for `GET /list`. 13 13 func (h *HttpServer) listEndpoint(w http.ResponseWriter, r *http.Request) { 14 14 cursor := 0 15 15 16 16 if c := r.URL.Query().Get("cursor"); c != "" { 17 17 parsed, err := strconv.Atoi(c) 18 18 if err != nil || parsed < 0 { 19 - http.Error(w, "invalid cursor", http.StatusBadRequest) 19 + writeJSONError(w, "invalid cursor", http.StatusBadRequest) 20 20 return 21 21 } 22 22 cursor = parsed ··· 50 50 } 51 51 } 52 52 53 - // buildListResponse format the api response from a slice of bitcoin prices 53 + // buildListResponse format the api response from a slice of bitcoin prices. 54 54 func buildListResponse(page []types.Price) types.ListResponse { 55 55 if len(page) == 0 { 56 56 return types.ListResponse{Data: []types.Entry{}}
+4 -4
internal/handlers/price.go
··· 9 9 "github.com/eagleusb/proxycon/internal/types" 10 10 ) 11 11 12 - // priceEndpoint is the handler function for `GET /price?at=<timestamp>` 12 + // priceEndpoint is the handler function for `GET /price?at=<timestamp>`. 13 13 func (h *HttpServer) priceEndpoint(w http.ResponseWriter, r *http.Request) { 14 14 at := r.URL.Query().Get("at") 15 15 if at == "" { 16 - http.Error(w, "missing query parameter: at", http.StatusBadRequest) 16 + writeJSONError(w, "missing query parameter: at", http.StatusBadRequest) 17 17 return 18 18 } 19 19 20 20 if _, err := time.Parse(time.RFC3339, at); err != nil { 21 - http.Error(w, "invalid timestamp: must be RFC3339 format", http.StatusBadRequest) 21 + writeJSONError(w, "invalid timestamp: must be RFC3339 format", http.StatusBadRequest) 22 22 return 23 23 } 24 24 ··· 38 38 } 39 39 } 40 40 41 - http.Error(w, "no price found for the given timestamp", http.StatusNotFound) 41 + writeJSONError(w, "no price found for the given timestamp", http.StatusNotFound) 42 42 }
+3 -1
internal/handlers/server.go
··· 14 14 serveMux *http.ServeMux 15 15 } 16 16 17 - // NewHttpServer returns the http api 17 + // NewHttpServer returns the http api. 18 18 func NewHttpServer(p *data.BitcoinPrices, pageSize int) *HttpServer { 19 19 return &HttpServer{ 20 20 datasource: p, ··· 23 23 } 24 24 } 25 25 26 + // Listen starts the http server with http2 support. 26 27 func (h *HttpServer) Listen(addr string) error { 27 28 h.registerHandler() 28 29 ··· 45 46 return nil 46 47 } 47 48 49 + // registerHandler registers all api handlers. 48 50 func (h *HttpServer) registerHandler() { 49 51 h.serveMux.HandleFunc("GET /list", h.listEndpoint) 50 52 h.serveMux.HandleFunc("GET /price", h.priceEndpoint)
+5
internal/types/model.go
··· 35 35 Avg string `json:"avg"` 36 36 Data []Entry `json:"data"` 37 37 } 38 + 39 + type ErrorResponse struct { 40 + Code int `json:"code"` 41 + Message string `json:"message"` 42 + }