#!/usr/bin/env python3 """Compare HEAD and GET on a disposable loopback HTTP server.""" import argparse import http.client import json import platform import threading from datetime import datetime, timezone from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path EXPECTED = b'RELEASE-EXPECTED-v2' class Handler(BaseHTTPRequestHandler): def log_message(self, *args): pass def respond(self, head): if self.path == '/ok': status, body = 200, b'

RELEASE-EXPECTED-v2

' elif self.path == '/wrong-content': status, body = 200, b'

RELEASE-OLD-v1

' elif self.path == '/method-mismatch': # Deliberately model an application that handles methods differently. status, body = (200, b'ok') if head else (503, b'unavailable') else: status, body = 404, b'not found' self.send_response(status) self.send_header('Content-Type', 'text/html; charset=utf-8') self.send_header('Content-Length', str(len(body))) self.end_headers() if not head: self.wfile.write(body) def do_HEAD(self): self.respond(True) def do_GET(self): self.respond(False) def main(): parser = argparse.ArgumentParser() parser.add_argument('--output', required=True) args = parser.parse_args() output = Path(args.output) if output.exists(): raise SystemExit('Existing evidence will not be overwritten.') server = ThreadingHTTPServer(('127.0.0.1', 0), Handler) thread = threading.Thread(target=server.serve_forever, kwargs={'poll_interval': 0.05}, daemon=True) thread.start() observations = [] try: for path in ['/ok', '/wrong-content', '/method-mismatch', '/missing']: pair = {'path': path} for method in ['HEAD', 'GET']: connection = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=3) try: connection.request(method, path) response = connection.getresponse() body = response.read() pair[method.lower()] = {'status': response.status, 'body_bytes': len(body), 'content_length': response.getheader('Content-Length'), 'expected_marker': EXPECTED in body, 'body': body.decode('utf-8')} finally: connection.close() observations.append(pair) assert all(row['head']['body_bytes'] == 0 for row in observations) assert observations[0]['get']['expected_marker'] assert observations[1]['head']['status'] == 200 and not observations[1]['get']['expected_marker'] assert observations[2]['head']['status'] == 200 and observations[2]['get']['status'] == 503 assert observations[3]['get']['status'] == 404 finally: server.shutdown() server.server_close() thread.join(timeout=3) labels = ['정상 본문', '다른 릴리스 본문', '의도적으로 다른 메서드 처리', '없는 경로'] rows = [[label, item['head']['status'], item['head']['body_bytes'], item['get']['status'], item['get']['body_bytes'], '있음' if item['get']['expected_marker'] else '없음'] for label, item in zip(labels, observations)] record = {'schema_version': 1, 'observed_at': datetime.now(timezone.utc).isoformat(), 'environment': {'os': platform.system(), 'python': platform.python_version(), 'server': 'Python ThreadingHTTPServer', 'binding': '127.0.0.1:자동 배정 포트'}, 'method': '임시 루프백 서버의 네 경로에 HEAD와 GET을 각각 요청하고 상태·본문 바이트 수·예상 릴리스 문자열을 비교했다. 잘못된 본문과 메서드별 분기는 실험을 위해 의도적으로 구현했다.', 'limits': '합성 서버에서의 기능 비교이며 운영 장애·CDN·TLS·부하·성능 측정이 아니다. 실제 서비스가 HEAD와 GET을 다르게 구현했다는 증거가 아니다.', 'observations': observations, 'tables': {'requests': {'headers': ['경로 조건', 'HEAD 상태', 'HEAD 본문 바이트', 'GET 상태', 'GET 본문 바이트', 'GET 예상 문자열'], 'rows': rows}}} output.parent.mkdir(parents=True, exist_ok=True) with output.open('x', encoding='utf-8') as stream: json.dump(record, stream, ensure_ascii=False, indent=2) stream.write('\n') print(json.dumps({'output': str(output), 'cases': len(rows), 'checks': 'passed'}, ensure_ascii=False)) if __name__ == '__main__': main()