From c24a530adfb4b9381cc5f51c9d939044df99be77 Mon Sep 17 00:00:00 2001 From: SuchetaVadakkepat10 Date: Wed, 17 Sep 2025 22:43:02 +0400 Subject: [PATCH 1/2] fix(error handling) : set up error handling for the API Endpoints --- .../__pycache__/app.cpython-310.pyc | Bin 3186 -> 3572 bytes .../__pycache__/squat_counter.cpython-310.pyc | Bin 6592 -> 6619 bytes optifit backend/app.py | 104 ++++++++++------ optifit backend/validation.py | 117 ++++++++++++++++++ 4 files changed, 181 insertions(+), 40 deletions(-) create mode 100644 optifit backend/validation.py diff --git a/optifit backend/__pycache__/app.cpython-310.pyc b/optifit backend/__pycache__/app.cpython-310.pyc index bc000b9c11242915a69b6256da57c5697aaa72ee..20ea4c90292659c808232c95bd8e1237e2762e6f 100644 GIT binary patch delta 2119 zcmZuy&2Jk;6yKTs@OooAX>2FXM_VTW6_--lst9PyXWKwIv=vS%m`K*vGfCF5y=Hct z(r8^n(_xl=58%K*K($vu>KSoBg|@smcH9=R*1Vm0^XBcF_xru)KXQN0 zr2J&k(eQcl^V;aXyQx|7>D`yNI!t3`Exn|1i)Xe-Da)+V1dEj>`A9X#Cz#FR+mvW( zHMzH94jZ|xm8LwcG|dt$3G*4At= z)I1d+*w@I@4>!@^zV(MTX&*$T=PpYbRzJbxO?+shG-w2Ti{b@EPzNjDw&Ae&Z;JL#)54WjqvF(0BhLznLP5 zvtMa-ZUQDz{F)o^ve#JgBf?d1eUU+{AM`9a)cr}HBMaRJ`mxyqpv1f}of*vB(V4YF z+mOo;ve!LkJm}mb>iPvD=D_qn?jmE&uk$xyBfx}Eol*?Zcw{|J#KloOa0o;N#&eGj zD+SybO!~li1mPM0rP8}OTI8a$!A0H+@-=UR&xs>oE?z-_Ie$jr-6EsuHC7j479css zsCX5PUQ%0^FBO-b3j?`>)uh6>50lN0r6wT=xIM6Fix5rfq1LGypd!+&TD_RGL$XWT zbg+supw_qAcNBDB+H*p>i^oPFor4`r>$cuDWI`r;DWqiEWG1sbB1c2>j3&pb`{a0N zhBVYeW5r;xZQ4ukYET4LXfiuoG+2DcXj?K9T3h6*R@4klW^b$$tsNuUW>QXsvFap@ z+!np6S=whBgPof(s37&ePOfVO=VJH1b&BM=>Dbbx9NbkP?0pU+WeQWJ$ql#WF<0_( zt6B411{D0F1qC1ws#1EmyWuhJmsh+R7cum4qWewk-OhfPMnP;WwE3E3|z`@PE-&SN1eK0mFif?YDz$HWQz&OsD;6d16Izm(i7lTH~T zcf~yJJSB$WL}G9qgY&{tKM^^X%v(P?6TMV|=d zhbVxk5DJmk0zm_lqz4q*1>S^&ktvN)XmV3h@0ZyQ?E^X3GhuH* z8VumQMZhKodK=Ef_v`?0^Tv4q96STyR!FNc81_@Zr`z-y3Or?~|YtQy`$NDN8kvmoh`ptNyhi9~7|qDj&d?zR-|dG~620 zDwJy9uVUQGvuf^-u=-z3@z|?r<+z@-a!X%p}=khNEfZfGi+4>E6Unu>ON~hLf=Nl1s{U2;Sa!p92kT?vGiHRUSx1W<;A83HAo76 bwqZCnO^rJwm4Kncc+nyD7z|^a8jt@4h}+Fv delta 1719 zcmZ8h%WoS+7@wJac)eabiR~oLtJ#!Sod_sUgeXxI)CyXVQjk+2Sw_amjFWYeU2Arn zs?l1ONDo{hQ7t!uu(@&Jz&}uN<-~cfTsUw-y?{!@H)AJCS!;ft?>#fWZ|3RTU!`1- z&DsdQC!Y^?e;a$8o5yz^uN?Q7$qL7~TBJsGlA6^imT671Nt&Y83BgE7Q|FS0swJAH z_9IlCVa3)g%}{3t)9eYMIXdb^X>HY} zz+RX6asWglgkXa8f6X&1&0mrMivZg&p8E($w_t=0fGq4`sx?UkmGzAzbpY#a?69M; z3gd@tox5J>?t6!9iN6T$mzE&U>hfaRp1-py)5`n~;jc)$8$U6pdMTKiFjUo_VM4J$ zEqsW1Ms6GG>$1MoQ9OgXNRSA%v}4@GLT?#;Q=}q1AYC$&ENVpfTl5_Pofe^fIwAv% zk+7+GZc**2b#;*u&VD~TfIjRRvQIrMa*?r$#8_)wOhiURA}!LJI?bGretv+UdF;rb zPNdW9soph3Au^Aloyxh5#85o)G1$RTej{*|=r+B5=C&JLcwb|L;$KoXaW;OI+Pszp zKVFpZvIN<2z$5A6GP$+J|Q(}vCCP5!r3O~rke6O zl^_&QksB~Cr+69u_=Ku7f2V;XanE4WLa_d`2}20e;|(fgHJETzPDxn)U-x zZw4Ko9%VlYwpyBD=s8)-Z!xJq4;WiXur@?7DbTaHGALR^MePztDXw)sq~f? zh08m3o=DBLAw${@8}UFC*;TC-Y+br1>Z;n{ROc-0?29iTPdE->6CLNVdKV}UyiRi%XlfSIB`*Bc~>TQ2O!af zcfjJmopLX+>UF?Bef(^AuPrvKe;qFHLs*N z#yvPV-&w)PG$y~GBr`3u1V$?)B_?O5=A~@D!uX4knJMq|=8Meg{EW9Y{}f_pW2~Ia nD5=Mt2DGIp1w;f-wvyD4iUTo=L4-1h-~kZ=Afj?|jid_z9Vjm& delta 130 zcmca@e88AHpO=@50SG4MPTk18j8WOx*(xTqIJKxa#xX4|F)zj?wK%&ZzaR!ERF+z# zP?B0)viUvZFGgnO!l=z}nAQ0iqc)2Ov$HW)OqP+<<4y${Rg?@Of+mMbYDmR`m_;B$ R8AR}a2mugLF?qVA3jj18BclKS diff --git a/optifit backend/app.py b/optifit backend/app.py index 78e5e9e..a7b622c 100644 --- a/optifit backend/app.py +++ b/optifit backend/app.py @@ -5,6 +5,7 @@ pass else: ssl._create_default_https_context = _create_unverified_https_context + from flask import Flask, request, send_file, jsonify, url_for import os import threading @@ -12,6 +13,7 @@ import time from werkzeug.utils import secure_filename from squat_counter import process_squat_video # Import the actual processing logic +from validation import * app = Flask(__name__) @@ -20,6 +22,17 @@ os.makedirs(UPLOAD_FOLDER, exist_ok=True) os.makedirs(PROCESSED_FOLDER, exist_ok=True) + +# Creates standardised error response +def error_response(message, status_code): + return jsonify({ + "success": False, + "error": True, + "message": message, + "status_code": status_code, + "timestamp": int(time.time()) + }), status_code + # In-memory job store: {job_id: {"status": "processing"/"done", "result": {...}}} jobs = {} @@ -40,7 +53,7 @@ def process_video_async(job_id, input_path, output_path, video_url): jobs[job_id]["error"] = str(e) print(f"Error in background processing: {e}") - +#Route to home @app.route('/', methods=['GET']) def home(): base_info = { @@ -51,57 +64,68 @@ def home(): "/result/": "GET - Check processing status and get results" } } - - return base_info, 200 + return jsonify(base_info), 200 + +#Route to ping the server @app.route('/ping', methods=['GET']) def ping(): - return {"message": "Server is live!"}, 200 + return jsonify({"message": "Server is live!"}), 200 +#Route to get upload the video @app.route('/upload', methods=['POST']) def upload_video(): - if 'video' not in request.files: - return {'error': 'No video file part'}, 400 + try: + video = validate_upload_request(request) + validate_video_file(video) - video = request.files['video'] - filename = secure_filename(video.filename) - input_path = os.path.join(UPLOAD_FOLDER, filename) - output_path = os.path.join(PROCESSED_FOLDER, f"processed_{filename}") - - # Save uploaded video - video.save(input_path) - - # Generate video URL - video_url = url_for('get_processed_video', filename=f"processed_{filename}", _external=True) - - # Create job - job_id = str(uuid.uuid4()) - jobs[job_id] = {"status": "processing"} - - # Start background processing with pre-generated URL - threading.Thread(target=process_video_async, args=(job_id, input_path, output_path, video_url)).start() + filename = secure_filename(video.filename) + input_path = os.path.join(UPLOAD_FOLDER, filename) + output_path = os.path.join(PROCESSED_FOLDER, f"processed_{filename}") + + # Save uploaded video + video.save(input_path) + + # Generate video URL + video_url = url_for('get_processed_video', filename=f"processed_{filename}", _external=True) + + # Create job + job_id = str(uuid.uuid4()) + jobs[job_id] = {"status": "processing"} + + # Start background processing with pre-generated URL + threading.Thread(target=process_video_async, args=(job_id, input_path, output_path, video_url)).start() + + response_data = { + "status": "processing", + "job_id": job_id, + "message": "Video uploaded successfully. Processing started.", + "video_url": video_url + } + + return jsonify(response_data) - response_data = { - "status": "processing", - "job_id": job_id, - "message": "Video uploaded successfully. Processing started.", - "video_url": video_url - } + except APIError as e: + return error_response(e.message, e.status_code) - return jsonify(response_data) + +# Route to get the result of the job with the job id @app.route('/result/', methods=['GET']) def get_result(job_id): - job = jobs.get(job_id) - if not job: - return jsonify({"status": "not_found", "error": "Job not found"}), 404 - - if job["status"] == "processing": - return jsonify({"status": "processing", "message": "Video is being processed..."}) - elif job["status"] == "error": - return jsonify({"status": "error", "error": job.get("error", "Unknown error")}), 500 - else: - return jsonify({"status": "done", "result": job["result"]}) + try: + validate_job_request(job_id,jobs) + + job = jobs.get(job_id) + if job["status"] == "processing": + return jsonify({"status": "processing", "message": "Video is being processed..."}) + elif job["status"] == "error": + raise InternalServerError("Unknown Error") + else: + return jsonify({"status": "done", "result": job["result"]}) + + except APIError as e: + return error_response(e.message, e.status_code) # New endpoint to serve processed videos by filename @app.route('/processed/') diff --git a/optifit backend/validation.py b/optifit backend/validation.py new file mode 100644 index 0000000..1740dc9 --- /dev/null +++ b/optifit backend/validation.py @@ -0,0 +1,117 @@ +import os +import uuid + +class APIError(Exception): + def __init__(self, message, status_code=500): + self.message = message + self.status_code = status_code + +class BadRequestError(APIError): + def __init__(self, message): + super().__init__(message, 400) + +class NotFoundError(APIError): + def __init__(self, message): + super().__init__(message, 404) + +class MethodNotAllowedError(APIError): + def __init__(self, message): + super().__init__(message, 405) + +class UnsupportedMediaTypeError(APIError): + def __init__(self, message): + super().__init__(message, 415) + +class InternalServerError(APIError): + def __init__(self, message): + super().__init__(message, 500) + +class PayloadTooLargeError(APIError): + def __init__(self, message): + super().__init__(message, 413) + + +# Configuration constants +MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB (removed extra * 1024) +ALLOWED_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.wmv'} + + +# Validating the upload request for the file +def validate_upload_request(request): + # Checks included -> valid method, video field in request, if video is provided + + if request.method != 'POST': + raise MethodNotAllowedError("Only POST method is allowed") + + if not request.files: + raise BadRequestError("No files sent in request") + + if 'video' not in request.files: + raise BadRequestError("No 'video' field found in request") + + video = request.files['video'] + + if not video: + raise BadRequestError("No video file provided") + + if not video.filename or video.filename.strip() == '': + raise BadRequestError("No video file selected") + + return video + + +# Validates the video file iteslf in terms of file size, extensions and filename +def validate_video_file(file): + # Calculating the file size + file.seek(0, 2) # Seek to end + file_size = file.tell() + file.seek(0) + + #Checks if the file size is within limits + if file_size == 0: + raise BadRequestError("Video file is empty") + + if file_size > MAX_FILE_SIZE: + raise PayloadTooLargeError( + f"File size ({file_size / (1024*1024):.1f}MB) exceeds maximum " + f"allowed size ({MAX_FILE_SIZE / (1024*1024)}MB)" + ) + + if not file.filename: + raise BadRequestError("File has no name") + + + # Validates for given extensions + file_ext = os.path.splitext(file.filename)[1].lower() + if file_ext not in ALLOWED_EXTENSIONS: + raise UnsupportedMediaTypeError( + f"Unsupported file format '{file_ext}'. " + f"Allowed formats: {', '.join(ALLOWED_EXTENSIONS)}" + ) + + +# Validating the job request for getting the results +def validate_job_request(job_id, jobs): + if not job_id: + raise BadRequestError("Job ID is required") + + # Validate UUID format + try: + uuid.UUID(job_id) + except ValueError: + raise BadRequestError("Invalid job ID format") + + job = jobs.get(job_id) + if not job: + raise NotFoundError("Job not found") + + +def validate_file_serving(filename): + if not filename: + raise BadRequestError("Filename is required") + + if '..' in filename or '/' in filename or '\\' in filename: + raise BadRequestError("Invalid filename - path traversal not allowed") + + return True + From 665e77a44a32b46338b94383227b7abbc17a505c Mon Sep 17 00:00:00 2001 From: SuchetaVadakkepat10 Date: Wed, 17 Sep 2025 22:53:12 +0400 Subject: [PATCH 2/2] fix(error handling) : set up the logging mechanism --- .../__pycache__/app.cpython-310.pyc | Bin 3572 -> 3817 bytes optifit backend/app.py | 6 ++++++ 2 files changed, 6 insertions(+) diff --git a/optifit backend/__pycache__/app.cpython-310.pyc b/optifit backend/__pycache__/app.cpython-310.pyc index 20ea4c90292659c808232c95bd8e1237e2762e6f..d7d5c63f164db6dbc6dba09e1c44ad45016897cf 100644 GIT binary patch delta 1508 zcmZuw&2L*p5P!3Nwx6G$oiwo>Cw3A$PMf#|g8CsyThj2+LMRPINd@)Aa=KgRIgX#T zwv#H92C3zMst8rf1yzXlfJ6}D!i`HggTx>3Bu+?(N*qxS6$ED1Z4xAG&F{_5?Ci|` zcIMB_!{N9Wi`fSLK4L#Df7W*=o`q|7j@;hl28Vj0ln^12ya}b0NYn;U(mb>UV2H%M zL6N)WQ7%(vZ4(&J*IYj}J>P+awd8%ilXFxyzlI$+o+yLZ;K)HGWk61EUE{g0z6W`hQ7V90mcVmFgCL7RBeFU zZK$R18a#SETpPS=d~WgpcZjw25Lr+%H`%*J$hd59jJpv=F7Cws8fuE@3wl5SxZox~5-y=qlRNxWj725)+9qunhi zg*N@VMXMBCbGh*3F*4-{@-!imYL=twok;&gfz&;MleqMqPP6V+cv%NuL`&-#bvrWA zGXg_keaLp?n-p|dJ%|j$*XozZ(xpY@8o5NSL$xq+ZPg;0YgV&0*A9jsf7UnH1DSd=Hp z7gImm1CUj}*@ey5$@c~UohJhXa|Ala9(Mu##HB#ZTay3BkzEz-vJ|aOy}gd%ElSLo z4K{P27aXu4!QyO}u?y(lNKQ4CjJ|*BH07hYkTV3c2u0{V(tlq<9!1y3D7wkhOdeH} z&S98T3y$j5r@V$v`3gb5dgM%RhN&^AiL7$DQCSt`vLC24n|>50*}d|r*SP9deRFR9 z+}nNxZ}3*PjWi!;=#*Im-&(D#2=2<3)O=8XK50hjb=smkx_aJpBhcM+rG8*Sb{hM- zX>VMu)2klu)Wrw#7>bE<$Oh{GirL4^oqqv# CFi(8| delta 1234 zcmZuv&u&Z+ho1|5yy!g7g0k2fvD0nZ3?BJsz|+r16W9_4Kq#FvAyoB z9jS;a{lTr}faYAO+-OyC;lz<^D^=>P+P{E`Ll5QD3g~-dNGPhU_i6Th@4b2NoA*cK zaXlZquC3wk+0R>r1Gno4Growl;7L)mzb~o3(8++mFMy6RaR5P zq9W$@66$PT`J3GTex5aM6Slw>m8Zo^thr6akC3s%T5O56u~kOam33Z|cLhquHV7;qF}zn7P4kD}q`d0-O?P^98G zM!qf|nr-sCbTW6nZK8diA=))}T)v*^Rlut9QKn98nXy($Ri3pj`c+I?Kv3eL4xO6( z#abaJr0@b_xrLv*a}j1o+o zME@c}69F$$_jX;ulTkbbvA&?s#96G=k!yB?d@HZm8{Rq40~QDLFt5{I2F#Lw+V!mcu~0Icl3uQz zQ%5C+E6?P-;}tAZ1L)9cm^x>>rJb-JLCZYo#T$X(pH6r(7Ii6e?Q`#A&MQjj({Xi( z{@)?(O+JP)$)rCXL3&b^1;@9Z&vITKbG}+##