Can't handle parallel connections anymore (stage #JM5)

I’m stuck on Stage #JM5

I’ve tried watching screencasts and other’s code about threading, but mine didn’t work even if the threading code was very similar.

Here are my logs:

[tester::#EJ5] Running tests for Stage #EJ5 (Concurrent connections)
[tester::#EJ5] Running program
[tester::#EJ5] $ ./your_server.sh
[tester::#EJ5] Creating 2 parallel connections
[tester::#EJ5] Creating connection 1
[your_program]
[your_program] [0] Using linux.
[your_program]
[your_program] [*] Listening on localhost:4221...
[your_program]
[your_program]
[tester::#EJ5] Creating connection 2
[tester::#EJ5] Sending first set of requests
[tester::#EJ5] client-2: $ curl -v http://localhost:4221/
[tester::#EJ5] client-2: > GET / HTTP/1.1
[tester::#EJ5] client-2: > Host: localhost:4221
[tester::#EJ5] client-2: >
[tester::#EJ5] client-2: Sent bytes: "GET / HTTP/1.1\r\nHost: localhost:4221\r\n\r\n"
[tester::#EJ5] Failed to read response:
[tester::#EJ5] Received: "" (no content received)
[tester::#EJ5]            ^ error
[tester::#EJ5] Error: Expected: HTTP-version, Received: ""
[tester::#EJ5] Test failed
[tester::#EJ5] Terminating program
[tester::#EJ5] Program terminated successfully

And here’s a snippet of my code:

import socket
import sys
import argparse
import threading

HTTP = "HTTP/1.1"
PATH_SLASH = '/'
STATUS = {
    200: "OK",
    201: "Created",
    404: "Not Found"
}
    
def main():
    # You can use print statements as follows for debugging, they'll be visible when running tests.
    # print("Logs from your program will appear here!")


   # IGNORE THIS CHUNK OF CODE, it is for another thing, not my problem

    which_os = "linux" # DON NOT FORGET AFTER DEBUG TO SWITCH IT TO NONE
    # check for slash symbol used in path
    while not(which_os == "linux" or which_os == "windows"):
        which_os = input("[0] Enter FULL name of the OS you are using.\nIt is very important for pulling/posting files!\n")
    if which_os.lower() == "windows":
        PATH_SLASH = '\\'
    print(f"\n[0] Using {which_os}.")



    IP = "localhost"
    PORT = 4221
    handled_connections = 0
    server_socket = socket.create_server((IP,PORT), reuse_port=True)

    # server_socket.accept() # wait for client
    print(f"\n[*] Listening on {IP}:{PORT}...\n\n")


    while True:
        connection, address = server_socket.accept()
        # print(f"[*] Connection accepted from {address[0]}:{address[1]}")

        req = Request().parse_from_string(connection.recv(2048).decode())

        target_request = req.path
        print(f"[R] [{address[0]}] requested {req.method} {req.path}")


        
        # thread = threading.Thread(args=(req, connection), target=handle_connections)
        # thread.start()

        threading.Thread(target=client_thread, args=(req, connection)).start()
        # threading.Thread(args=(req, connection), target=handle_connections).start()

        # handled_connections += 1
        # print(f"\n\n[H] Connections handled: {handled_connections}\n\n")

def client_thread(req, connection):
    print(f"[*] Connection established on {connection}")
    handle_connections(req, connection)

class Request:
    def __init__(self):
        self.protocol = HTTP
        self.method = "GET"
        self.path = None
        self.headers = None
        self.body = ""
  
    def get_header_value(self, header_name: str):
        try:
            return self.headers[header_name]
        except:
            return None
    
    def get_encodings(self) -> list:
        encodings = self.get_header_value("Accept-Encoding")

        if encodings == None:
            return list()
        else:
            return encodings.split(', ')

    def parse_from_string(self, request: str):
        lines = request.split('\r\n') 
        method, path, protocol = lines[0].split(' ')
        body = lines[-1]

        header_cnt = 0
        for i in lines:
            if i.find(": ") != -1:
                header_cnt += 1

        headers = {}
        # the first line is method, path, protocol
        # the last line is body

        # other lines in between first
        for i in range(header_cnt):
            header_name = lines[i+1].split(': ')[0]
            header_value = lines[i+1].split(": ")[1]

            # headers.update({lines[i+1].split(": ")[0] : lines[i+1].split(": ")[1]})
            headers.update({header_name: header_value})


        self.path = path
        self.method = method
        self.headers = headers
        self.body = body

        print(f"FROM PARSE_FROM_STRING lines are: {lines}")


        return self


class Response:
    def __init__(self, supported_encodings = ["gzip"]):
        self.protocol = HTTP
        self.status_code = 200
        self.body = ""
        self.headers = {} # dict
        self.supported_encodings = supported_encodings
        
    
    
    def with_header(self, header_name, header_value):
        self.headers.update({header_name: header_value})
        return self


    def with_protocol(self, protocol):
        self.protocol = protocol
        return self
    
    def with_content_type(self, content_type):
        self.with_header("Content-Type", content_type)
        return self
    
    def with_body(self, body):
        if body == None:
            self.body = ""
        else:
            self.body = body
        return self
    
    def with_encoding(self, list_of_encodings):
        for encoding in list_of_encodings:
            if encoding in self.supported_encodings:
                self.with_header("Content-Encoding", encoding)
                break

        return self
    
    def with_status_code(self, code):
        self.status_code = code
        return self
    
    def build(self):
        self.with_header("Content-Length", len(self.body))

        headers = map(lambda kv: f'{kv[0]}: {kv[1]}\r\n', self.headers.items())
        headers = ''.join(headers)

        response = f"{self.protocol} {self.status_code} {STATUS[self.status_code]}\r\n{headers}\r\n{self.body}"
        return response

def handle_connections(request: Request, connection: socket):
    target_request = request.path
    response = ''
    protocol = request.protocol

    encodings = request.get_encodings()

    # print(f"Request method is: [{request.method}]")
    STATUS_NOT_FOUND = "HTTP/1.1 404 Not Found\r\n\r\n"
    
    # argv[2] takes the second argument of the command line: ./your_server.sh --directory /tmp/
    # ./your_server.sh is argument N0, so /tmp/ is N2 since indexing starts with 0
    # if there are more than 2 arguments, there is a path in the command line
    FILE_PATH = sys.argv[2] if len(sys.argv) > 2 else f".{PATH_SLASH}files/"

    if request.path == '/':
        response = Response().with_content_type("text/plain").build()

    elif request.path.startswith("/echo/"):
        echo = request.path[len("/echo/"):]
        response = Response().with_content_type("text/plain").with_body(echo).with_status_code(200).with_encoding(encodings).build()

    elif request.path == '/user-agent':
        response = Response().with_content_type("text/plain").with_body(request.body).with_encoding(encodings).build()
    
    elif request.path.startswith("/files/"):
        file_name = target_request[len("/files/"):]
        # if i search for a file in a folder that is in ./files, i might get an error on Windows
        
        if request.method == "GET":
            # print(f"[R] File \"{file_name}\" requested.")
            # try block makes that if there is an error it will execute the except statement 
            try:
                with open(f"{FILE_PATH}{file_name}", 'r') as file:
                    file_content = file.read()
                    # response = create_response(status_code_ok, status_message, "application/octet-stream", file_content)
                    response = Response().with_content_type("application/octet-stream").with_body(file_content).build()
            except:
                response = "HTTP/1.1 404 Not Found\r\n\r\n"
        
        elif request.method == "POST":
            print(f"FILE PATH IS: {FILE_PATH}{file_name}")
            create_file(f"{FILE_PATH}{file_name}", request.body)
            response = Response().with_status_code(201).build()
            
            
    else:
        response = Response().with_status_code(404).build()
    
    # print(f"[D] RESPONSE:\r\n{response}")
    connection.sendall(response.encode())
    connection.close()


def create_file(FILE_PATH: str, file_content):
    new_file = open(FILE_PATH, "w")
    new_file.write(file_content)
    new_file.close()
    



if __name__ == "__main__":
    main()

@erikvelf could you try adding debug logs? That’ll help track down what’s happening here. Particularly around (a) when a new connection is accepted (b) bytes are read from a connection.

More on how to add debug logs here: How do I debug test failures? - CodeCrafters

1 Like

Appreciate your help but I found the issue.

Actually my program was reading inputs in the main thread and that prevented parallel connections.
So the tests just worked magically for multithreading, but they shouldn’t.
Now I believe they fixed the tests.

Still thanks for your effort!

1 Like

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.