3

I am trying to write a command line utility that will send a HTTP request, and print the response body in the terminal. The endpoint I'm hitting is supposed to return a JSON body

{"arr":[{"name":"Google","url":"https://www.google.com/"},{"name":"Instagram","url":"https://www.instagram.com/"},{"name":"Pinterest","url":"https://www.pinterest.com/"},{"name":"YouTube","url":"https://www.youtube.com/"}]}

Here is my code, body is the byte array which is the response body

fmt.Println(body)

var dat map[string]interface{}
if err := json.Unmarshal(body, &dat); err != nil {
    panic(err)
}
fmt.Println(dat)

The Println(body) function returns

[31 139 8 0 0 0 0 0 0 3 170 86 74 44 42 82 178 138 174 86 202 75 204 77 85 178 82 114 207 207 79 207 73 85 210 81 42 45 202 81 178 82 202 40 41 
41 40 182 210 215 47 47 47 215 75 7 75 233 37 231 231 234 43 213 234 192 117 120 230 21 151 36 166 23 37 230 98 213 148 9 147 197 208 23 144 153 87 146 90 148 90 92 130 85 95 1 76 22 67 95 100 126 105 72 105 18 118 39 86 230 151 150 148 38 193 220 24 91 11 0 0 0 255 255 3 0 64 164 107 195 223 0 0 0]

and then I get this error

panic: invalid character '\x1f' looking for beginning of value

Am I misunderstanding the json.Unmarshall function? How can I decode that byte array and print the resulting json object to the terminal?

Here is the full code

package main 

import (
    "flag"
    "fmt"
    "os"
    "net"
    "bufio"
    "strings"
    "strconv"
    "net/url"
    "encoding/json"
    "io"
)

func main() {
    
    urlPtr := flag.String("url", "", "URL to fetch. (required)")
    flag.Parse()

    if (*urlPtr == "") {
        flag.PrintDefaults()
        os.Exit(1)
    }

    u, err := url.Parse(*urlPtr)

    if err != nil {
        fmt.Println("Error parsing url")
        os.Exit(1)
    }

    path := u.Path
    hostname := u.Hostname()
    
    conn, err := net.Dial("tcp", hostname + ":80")
    if err != nil {
        fmt.Println("Error setting up tcp connection")
        os.Exit(1)
    }

    if path == "" {
        path = "/"
    }

    request := "GET " + path + " HTTP/1.1\r\n" + 
                "Host: " + hostname + ":80\r\n" + 
                "User-Agent: Go-Runtime\r\n" + 
                "Accept: */*\r\n" +
                "Accept-Encoding: gzip, deflate, br\r\n" + 
                "Connection: keep-alive\r\n\r\n"

    _, err = fmt.Fprintf(conn, request)
    if err != nil {
        fmt.Println("Error sending request to serrver")
        os.Exit(1)
    }

    r := bufio.NewReader(conn)
    var content_length int
    content_bool := false
    transfer_encoding := false 
    for {
        line, err := r.ReadString('\n')
        
        if err != nil {
            fmt.Println("Error reading header line")
            os.Exit(1)
        }

        header := strings.Split(line, ": ")      
        if header[0] == "Content-Length" {
            content_length, err = strconv.Atoi(header[1][:len(header[1]) - 2])
            if err != nil {
                fmt.Println("Error reading content length")
                os.Exit(1)
            }
            content_bool = true
        }

        if (header[0] == "Transfer-Encoding") {
            transfer_encoding = true
        }
        
        if line == "\r\n" {
            break
        }
    }

    var body []byte
    var n int

    if content_bool == true {
        body = make([]byte, content_length)
        n, err = r.Read(body)
        if err != nil {
            fmt.Println("Error reading body")
            os.Exit(1)
        }
        fmt.Println(string(body[:n]))
    }

    if transfer_encoding == true {
        
        for {
            line, err := r.ReadString('\n')
            if err != nil {
                fmt.Println("Error reading length of chunk")
                os.Exit(1)
            }
            num := line[:len(line) - 2]
            if part_length, err := strconv.ParseInt(num, 16, 64); err == nil {

                if part_length <= 0 {
                    break
                }
                body_part := make([]byte, part_length)
                if _, err := io.ReadFull(r, body_part); err != nil {
                    fmt.Println("Error reading chunk data")
                    os.Exit(1)
                }
                body = append(body, body_part...)
                _, err = r.Discard(2)
                if err != nil {
                    fmt.Println("Failed to discard trailing CRLF of chunk")
                    os.Exit(1)
                }
            } else {
                break
            }
        }

        fmt.Println(body)

        var dat map[string]interface{}
        if err := json.Unmarshal(body, &dat); err != nil {
            panic(err)
        }
        fmt.Println(dat)
        
    }

}
Rockstar5645
  • 4,376
  • 8
  • 37
  • 62
  • 2
    My gosh, that's some over-complex code. Why are you handling transfer encoding yourself, instead of letting the standard library handle it? – Jonathan Hall Oct 21 '20 at 09:34
  • It's for a coding assignment, they said we're not supposed to use the standard library – Rockstar5645 Oct 21 '20 at 09:34
  • 3
    So the simple answer to your question is: You get a JSON error because you're providing invalid JSON as input. If your question is about how to make the rest of the code work, I'm afraid you'll need to focus on something more specific. – Jonathan Hall Oct 21 '20 at 09:35
  • this is the specific endpoint I'm targetting: https://rockstar.akhilhello.workers.dev/links, can you help me write the go code to parse this json and display it on the terminal? – Rockstar5645 Oct 21 '20 at 09:36
  • No, we're not going to do your coding assignment for you. – Jonathan Hall Oct 21 '20 at 09:37
  • 1
    The problem you have is _not_ about parsing JSON. It seems to be with parsing an HTTP response. – Jonathan Hall Oct 21 '20 at 09:38
  • 1
    I'm with Flimzy: it seems the problem with JSON parsing is created by improper parsing of the HTTP response. We need to have that done properly first and then move on to the parsing. Hopefully _that_ problem will evaporate once you'll have the response parsing done right. – kostix Oct 21 '20 at 09:40
  • Can you guys tell me what might be wrong with the parsing I'm doing? Because I'm stuck right now. – Rockstar5645 Oct 21 '20 at 09:44
  • My comment regarding the semantics of `io.Reader.Read` (and `io.ReadFull`) plus scrupulous error handling; then let's try to see whether that helped. – kostix Oct 21 '20 at 10:06
  • @kostix I added the error handling, and I'm not getting any errors in that part of the code. I also changed the Read method to ReadFull to read the chunk. I am still getting the same error, I think it's parsing the response correctly, but I'm not able to parse the body of the response as JSON. – Rockstar5645 Oct 21 '20 at 21:41
  • What service is generating the JSON? – kostix Oct 22 '20 at 08:15
  • https://rockstar.akhilhello.workers.dev/links is the specific endpoint – Rockstar5645 Oct 22 '20 at 20:23
  • @kostix so it turns out, I was reading the chunks properly, they were just compressed using gzip because "Proper gzip data start with the magic sequence 0x1f 0x8b" (https://stackoverflow.com/a/55672915/4944292), I uncompressed them and was able to extract the json object – Rockstar5645 Oct 22 '20 at 20:32
  • Good find! But it seems you then did not pay attenton to the `Content-Encoding` field in the response's header. One more thing: what do you call "chunks", precisely? Are you talking about the parts of a multiple-part response or are you talking about the chunks of the so-called [`chunked` HTTP transfer encoding](https://tools.ietf.org/html/rfc7230#section-4.1)? – kostix Oct 23 '20 at 09:30
  • Oh yeah, I was going through the goloang implementation of the http package to see how they were dealing with transfer encoding, and there was this part about uncompressing the body, and I didn't even know I had to do that. Then I read about the Content-Encoding header. I'm referring to the `chunked HTTP transfer encoding` – Rockstar5645 Oct 23 '20 at 17:14
  • @Flimzy I wasn't asked you to write the code, I just wanted some guidance – Rockstar5645 Nov 16 '20 at 02:22

2 Answers2

4

The reason why you get this error is you add request header "Accept-Encoding: gzip, deflate, br\r\n".

So gzip is the first priority for the response encoding.

You can check the response encoding by reading the content-encoding in header.

The first byte '\x1f' is actualy 1 of the 2 magic bytes '0x1f8b' of gzip content.

So to handle the gzip you will need to decompress the repsonse before reading them as plain text.

Here is the sample code:


    // Check if the response is encoded in gzip format
    if resp.Header.Get("Content-Encoding") == "gzip" {
        reader, err := gzip.NewReader(resp.Body)
        if err != nil {
            panic(err)
        }
        defer reader.Close()
        // Read the decompressed response body
        body, err := io.ReadAll(reader)
        if err != nil {
            panic(err)
        }
        // Do something with the response body
        fmt.Println(string(body))
    } else {
        // The response is not gzip encoded, so read it directly
        body, err := io.ReadAll(resp.Body)
        if err != nil {
            panic(err)
        }
        // Do something with the response body
        fmt.Println(string(body))
    }
}
Dung Le
  • 104
  • 3
1

\x1f is the "UNIT SEPARATOR" — a control character from the lower part of the ASCII table.

I dunno how it ended up in the response but I might guess that the endpoint might send you multiple JSON documents in a row — separating them with 0x1F, that is, doing a variation of this.

Another possibility is that you have some error with getting the data. You might show us the code so we could try to make a better guess.

kostix
  • 51,517
  • 14
  • 93
  • 176
  • 2
    @Rockstar5645, any reason you're not using `net/http` to carry out the request? – kostix Oct 21 '20 at 09:34
  • it's for a coding assignment, they said we're not allowed to use the standard library, we have to implement it ourselves – Rockstar5645 Oct 21 '20 at 09:35
  • 2
    @Rockstar5645, one obvious problem with your code then is that `io.Reader.Read` does not do what you appear to think it does. Please read [this](https://golang.org/pkg/io/#Reader) carefully and compare it with [this](https://golang.org/pkg/io/#ReadFull). I also hope you have elided error handling only for the purpose of brevity of the post. – kostix Oct 21 '20 at 09:37
  • I don't know what brewity means but I just went through the go tour yesterday, and I'm not that good at error handling yet, but I'm working on it – Rockstar5645 Oct 21 '20 at 09:39
  • 1
    Sorry, I meant brevity. As to your experience, it does not matter how much you have, really. You ask questions, we give advice, you follow it (or you don't); there's no need for excuses as no one blamed you. – kostix Oct 21 '20 at 09:42
  • Oh yeah, I'll try and learn error handling as soon as I get this response parser to work. – Rockstar5645 Oct 21 '20 at 09:43
  • 1
    You cannot get the latter working without the former. Any error you're ignoring renders the code past the statement which produced an error as having undefined behaviour. Consider: you call `someLength, _ := strconv.Atoi(some_string)`; how do you know what value `someLength` would contain if the call to `Atoi` failed and returned an error you have ignored? What if it's `-1`? `42`? The same applies to `conn.Read`: what if it reads three bytes and then detects the connection was closed on the remote end and reports that. How sensible it is to continue processing as if nothing has happened? – kostix Oct 21 '20 at 10:20