goblockchain

Go Blockchain API: invalid character 'þ' looking for beginning of value


I am working on a property rental blockchain, with an API and when I make GET requests locally to localhost:8080/contracts I receive the error: "invalid character 'þ' looking for beginning of value". I receive this error in curl and Postman.

Main.go:

package main

import (
    "bytes"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "time"

    "github.com/HartleyIntegrity/smartley-chain/api"
    "github.com/HartleyIntegrity/smartley-chain/db"
)

func main() {
    // Connect to MongoDB
    err := db.ConnectMongoDB()
    if err != nil {
        log.Fatalf("Error connecting to MongoDB: %v", err)
    }

    // Create a new router
    router := api.NewRouter()

    // Logging middleware
    router.Use(func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()

            // Create a response logger to log response details
            resLogger := &responseLogger{
                ResponseWriter: w,
                statusCode:     http.StatusOK,
            }

            next.ServeHTTP(resLogger, r)

            // Log the request and response details to console and file
            logDetails := fmt.Sprintf("%s %s %d %v %s\n", r.Method, r.URL.Path, resLogger.statusCode, time.Since(start), resLogger.body)
            fmt.Print(logDetails)
            log.Print(logDetails)
        })
    })

    // Recovery middleware to log panics and recover from them
    router.Use(func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            defer func() {
                if r := recover(); r != nil {
                    errMsg := fmt.Sprintf("Panic: %v", r)
                    log.Println(errMsg)
                    http.Error(w, errMsg, http.StatusInternalServerError)
                }
            }()

            next.ServeHTTP(w, r)
        })
    })

    // Add logging to dump the request body to help debug "invalid character 'þ' looking for beginning of value" error
    router.Use(func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            bodyBytes, err := ioutil.ReadAll(r.Body)
            if err != nil {
                log.Printf("Error reading request body: %v", err)
            }
            log.Printf("Request body: %s", string(bodyBytes))
            r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

            next.ServeHTTP(w, r)
        })
    })

    // Start the server
    log.Println("Starting the server on port 8080...")
    log.Fatal(http.ListenAndServe(":8080", router))
}

// Define a custom response logger that logs response details
type responseLogger struct {
    http.ResponseWriter
    statusCode int
    body       string
}

func (rl *responseLogger) WriteHeader(statusCode int) {
    rl.statusCode = statusCode
    rl.ResponseWriter.WriteHeader(statusCode)
}

func (rl *responseLogger) Write(body []byte) (int, error) {
    rl.body = string(body)
    return rl.ResponseWriter.Write(body)
}

Routes.go

package api

import (
    "github.com/gorilla/mux"
)

func NewRouter() *mux.Router {
    router := mux.NewRouter().StrictSlash(true)

    router.HandleFunc("/contracts", createContractHandler).Methods("POST")
    router.HandleFunc("/contracts/{id}", getContractHandler).Methods("GET")
    router.HandleFunc("/contracts", listContractsHandler).Methods("GET")
    router.HandleFunc("/contracts/{id}", updateContractHandler).Methods("PUT")
    router.HandleFunc("/contracts/{id}", deleteContractHandler).Methods("DELETE")

    return router
}

Handlers.go

package api

import (
    "bytes"
    "encoding/json"
    "log"
    "net/http"

    "github.com/HartleyIntegrity/smartley-chain/blockchain"
    "github.com/HartleyIntegrity/smartley-chain/contracts"
    "github.com/gorilla/mux"
)

func respondWithError(w http.ResponseWriter, code int, message string) {
    respondWithJSON(w, code, map[string]string{"error": message})
}

func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
    response, err := json.Marshal(payload)
    if err != nil {
        respondWithError(w, http.StatusInternalServerError, err.Error())
        return
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    w.Write(response)
}

func createContractHandler(w http.ResponseWriter, r *http.Request) {
    // Log the request body to help identify any non-UTF characters causing issues
    defer r.Body.Close()
    buf := new(bytes.Buffer)
    buf.ReadFrom(r.Body)
    log.Printf("Request body: %s", buf.String())

    // Parse the request body to get the contract data
    var contractData blockchain.Contract
    err := json.NewDecoder(buf).Decode(&contractData)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // Create a new contract using the provided data
    newContract := contracts.CreateContract(contractData)

    // Respond with the newly created contract
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(newContract)
}

func getContractHandler(w http.ResponseWriter, r *http.Request) {
    // Get the ID from the URL
    vars := mux.Vars(r)
    id := vars["id"]

    // Get the contract with the provided ID
    contract, err := contracts.GetContract(id)
    if err != nil {
        http.Error(w, err.Error(), http.StatusNotFound)
        return
    }

    // Respond with the contract
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(contract)
}

func listContractsHandler(w http.ResponseWriter, r *http.Request) {
    // Log the request body to help identify any non-UTF characters causing issues
    defer r.Body.Close()
    buf := new(bytes.Buffer)
    buf.ReadFrom(r.Body)
    log.Printf("Request body: %s", buf.String())

    // Get a list of all contracts
    contracts, err := contracts.ListContracts()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Respond with the list of contracts
    w.Header().Set("Content-Type", "application/json")
    buf.Reset()
    err = json.NewEncoder(buf).Encode(contracts)
    if err != nil {
        log.Printf("Error encoding response body: %v", err)
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    responseBody := buf.String()
    w.WriteHeader(http.StatusOK)
    if _, err := w.Write(buf.Bytes()); err != nil {
        log.Printf("Error writing response body: %v", err)
    }
    log.Printf("Response body: %s", responseBody)
}

func updateContractHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id := vars["id"]

    var newContractData contracts.Contract
    if err := json.NewDecoder(r.Body).Decode(&newContractData); err != nil {
        respondWithError(w, http.StatusBadRequest, err.Error())
        return
    }
    defer r.Body.Close()

    contract, err := contracts.UpdateContract(id, newContractData)
    if err != nil {
        respondWithError(w, http.StatusNotFound, err.Error())
        return
    }

    respondWithJSON(w, http.StatusOK, contract)
}

func deleteContractHandler(w http.ResponseWriter, r *http.Request) {
    // Get the ID from the URL
    vars := mux.Vars(r)
    id := vars["id"]

    // Delete the contract with the provided ID
    err := contracts.DeleteContract(id)
    if err != nil {
        http.Error(w, err.Error(), http.StatusNotFound)
        return
    }

    // Respond with a 204 No Content status to indicate that the contract was successfully deleted
    w.WriteHeader(http.StatusNoContent)
}

Blockchain.go

package blockchain

import (
    "bytes"
    "crypto/rand"
    "crypto/sha256"
    "encoding/gob"
    "encoding/hex"
    "encoding/json"
    "errors"
    "fmt"
    "log"
    "time"
)

type Blockchain struct {
    Blocks []*Block
}

func CreateBlock(index int, data Contract, prevHash string) *Block {
    timestamp := time.Now()
    dataBytes := data.ToBytes()
    hash := CalculateHash(data, prevHash, timestamp)
    return &Block{index, timestamp.Format(time.RFC3339), dataBytes, hash, prevHash}
}

func (bc *Blockchain) AddBlock(contract Contract, prevHash string) {
    prevBlock := bc.Blocks[len(bc.Blocks)-1]
    dataBytes := prevBlock.Data
    var data Contract
    err := json.Unmarshal(dataBytes, &data)
    if err != nil {
        log.Fatal(err)
    }
    newBlock := CreateBlock(len(bc.Blocks), data, prevBlock.Hash)
    bc.Blocks = append(bc.Blocks, newBlock)
}

func CalculateHash(data Contract, prevHash string, timestamp time.Time) string {
    record := fmt.Sprintf("%s%d%d%s%s%s%s%f%f%f%t%s%s",
        data.ID,
        data.StartTime,
        data.ContractDuration,
        data.EndTime,
        data.CurrentStage,
        data.PropertyHash,
        data.PropertyID,
        data.PropertyAddress,
        data.RentAmount,
        data.DepositAmount,
        data.DepositDeductions,
        data.ContractSigned,
        prevHash,
        timestamp.Format(time.RFC3339),
    )
    h := sha256.New()
    h.Write([]byte(record))
    hashed := h.Sum(nil)
    return hex.EncodeToString(hashed)
}

func GenerateID() string {
    uuid := make([]byte, 16)
    _, err := rand.Read(uuid)
    if err != nil {
        log.Fatal(err)
    }
    return hex.EncodeToString(uuid)
}

func StageCreated() string {
    return "Created"
}

func NewBlockchain() *Blockchain {
    genesisBlock := CreateGenesisBlock()
    return &Blockchain{Blocks: []*Block{genesisBlock}}
}

func (c *Contract) ToBytes() []byte {
    var buf bytes.Buffer
    enc := gob.NewEncoder(&buf)
    if err := enc.Encode(c); err != nil {
        log.Fatalf("failed to encode contract: %v", err)
    }
    return buf.Bytes()
}

func CreateGenesisBlock() *Block {
    contract := Contract{
        ID:                GenerateID(),
        StartTime:         time.Now().Unix(),
        ContractDuration:  12,
        EndTime:           time.Now().AddDate(1, 0, 0).Unix(),
        CurrentStage:      StageCreated(),
        PropertyHash:      "",
        PropertyID:        "",
        PropertyAddress:   "",
        RentAmount:        0,
        DepositAmount:     0,
        DepositDeductions: 0,
        IssuesReported:    make([]Issue, 0),
        NoticesSent:       make([]Notice, 0),
        ContractSigned:    false,
    }
    block := CreateBlock(0, contract, "")
    return block
}

func (bc *Blockchain) ReplaceBlock(oldBlock, newBlock *Block) error {
    // Verify that the new block is valid
    if !isValidNewBlock(newBlock, oldBlock) {
        return errors.New("invalid block")
    }

    // Replace the old block with the new block
    for i, b := range bc.Blocks {
        if b.Hash == oldBlock.Hash {
            bc.Blocks[i] = newBlock
            return nil
        }
    }

    return errors.New("block not found")
}

func isValidNewBlock(newBlock, previousBlock *Block) bool {
    // Check that the block indices are consecutive
    if newBlock.Index != previousBlock.Index+1 {
        return false
    }

    // Check that the previous block's hash matches the new block's prevHash
    if newBlock.PrevHash != previousBlock.Hash {
        return false
    }

    // Check that the new block's hash is valid
    newTimestamp, err := time.Parse(time.RFC3339, newBlock.Timestamp)
    if err != nil {
        return false
    }
    // Decode the contract data from the block
    var data Contract
    err = json.Unmarshal(newBlock.Data, &data)
    if err != nil {
        return false
    }

    // Check that the new block's hash is valid
    if CalculateHash(data, newBlock.PrevHash, newTimestamp) != newBlock.Hash {
        return false
    }

    return true
}

I have tried setting various headers in my request such as content-type application/json etc. and nothing seems to work. I've asked chatgpt several times to try a solution and it just says there is an UTF-8 issue in a JSON response somewhere and I cannot for the life of me figure this out.

Thank you.

Github repo for reference:

https://github.com/HartleyIntegrity/smartley-chain


Solution

  • The error is happening because on blockchain.go#line22 you're setting the struct bytes, not a JSON text.

    func CreateBlock(index int, data Contract, prevHash string) *Block {
        timestamp := time.Now()
        // This is struct bytes, change to 'json.Marshal(data)'
        dataBytes := data.ToBytes() 
        hash := CalculateHash(data, prevHash, timestamp)
        return &Block{index, timestamp.Format(time.RFC3339), dataBytes, hash, prevHash}
    }
    

    It should be json.Marshal(data).

    Later in the code you're parsing bytes, but the content is bytes from a struct, not JSON text.

    func ListContracts() ([]Contract, error) {
        bc := blockchain.NewBlockchain()
        contracts := make([]Contract, 0)
        blocks := bc.Blocks
        for i := len(blocks) - 1; i >= 0; i-- {
            block := blocks[i]
            var contract blockchain.Contract
            // Error happens here
            err := json.Unmarshal(block.Data, &contract)
            if err != nil {
                return nil, err
            }
            contracts = append(contracts, Contract{contract})
        }
        return contracts, nil
    }
    

    Then you get the error.

    That being said, you should review your approach as carrying out the JSON text in that manner seems unnecessary, but changing that for now should work.

    I've sent this PR to your repo if you want to test it from there.